diff options
author | Niels De Graef <nielsdegraef@gmail.com> | 2022-08-16 12:36:08 +0200 |
---|---|---|
committer | Niels De Graef <nielsdegraef@gmail.com> | 2022-09-03 08:50:19 +0200 |
commit | 1d44c11483e3d00611c0beed9afc8f2c9facc3a8 (patch) | |
tree | 6a38fecd4acca16fb8c6d1b4322b2e10fbe0d5ad /src/core | |
parent | 4d0710b8a6c7a3cd2ee0311b62f5e2707c34c305 (diff) | |
download | gnome-contacts-1d44c11483e3d00611c0beed9afc8f2c9facc3a8.tar.gz |
Introduce the concept of Contacts.Chunk
This commit introduces a new class `Contacts.Chunk`. Just like libfolks,
we see a contact as a collection of data, or to word it differently: a
collection built up from "chunks" of information.
The net result of adding this concept adds quite a bit of lines of code,
but it does have some major benefits:
* Rather than stuffing new properties into yet another if-else spread
out over multiple places in contacts-utils (and quite a bit of other
files), we can create a new subclass of `Contacts.Chunk`
* This also goes for property-specific logic, which we can consolidate
within their appropriate classes/files.
* All of our logic is now unit-testable
In the future, this would allow for more cleanups/features:
* We can put the serialization code for each property inside the
`Contacts.Chunk`
* We can extend ContactSheet to show a vCard's information, before
actually importing it into a Folks.Individual.
* We can write unit tests on the set of chunks, rather than regularly
having to deal with yet another regression in e.g. the birthday
editor.
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/contacts-addresses-chunk.vala | 138 | ||||
-rw-r--r-- | src/core/contacts-alias-chunk.vala | 57 | ||||
-rw-r--r-- | src/core/contacts-avatar-chunk.vala | 53 | ||||
-rw-r--r-- | src/core/contacts-bin-chunk.vala | 171 | ||||
-rw-r--r-- | src/core/contacts-birthday-chunk.vala | 62 | ||||
-rw-r--r-- | src/core/contacts-chunk.vala | 63 | ||||
-rw-r--r-- | src/core/contacts-contact.vala | 303 | ||||
-rw-r--r-- | src/core/contacts-email-addresses-chunk.vala | 93 | ||||
-rw-r--r-- | src/core/contacts-full-name-chunk.vala | 61 | ||||
-rw-r--r-- | src/core/contacts-im-addresses-chunk.vala | 99 | ||||
-rw-r--r-- | src/core/contacts-nickname-chunk.vala | 60 | ||||
-rw-r--r-- | src/core/contacts-notes-chunk.vala | 85 | ||||
-rw-r--r-- | src/core/contacts-phones-chunk.vala | 97 | ||||
-rw-r--r-- | src/core/contacts-roles-chunk.vala | 94 | ||||
-rw-r--r-- | src/core/contacts-structured-name-chunk.vala | 69 | ||||
-rw-r--r-- | src/core/contacts-urls-chunk.vala | 95 |
16 files changed, 1600 insertions, 0 deletions
diff --git a/src/core/contacts-addresses-chunk.vala b/src/core/contacts-addresses-chunk.vala new file mode 100644 index 0000000..c322e91 --- /dev/null +++ b/src/core/contacts-addresses-chunk.vala @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the postal addresses of a contact (similar + * to {@link Folks.PostalAddressDetails}}. Each element is a {@link Address}. + */ +public class Contacts.AddressesChunk : BinChunk { + + public override string property_name { get { return "postal-addresses"; } } + + construct { + if (persona != null) { + return_if_fail (persona is PostalAddressDetails); + unowned var postal_address_details = (PostalAddressDetails) persona; + + foreach (var address_field in postal_address_details.postal_addresses) { + var address = new Address.from_field_details (address_field); + add_child (address); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new Address (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is PostalAddressDetails) { + var afds = (Gee.Set<PostalAddressFieldDetails>) get_abstract_field_details (); + yield ((PostalAddressDetails) this.persona).change_postal_addresses (afds); + } +} + +public class Contacts.Address : BinChunkChild { + + public PostalAddress address { + get { return this._address; } + set { + if (this._address.equal (value)) + return; + + bool was_empty = this._address.is_empty (); + this._address = value; + notify_property ("address"); + if (was_empty != value.is_empty ()) + notify_property ("is-empty"); + } + } + private PostalAddress _address = new PostalAddress ("", "", "", "", "", "", "", "", ""); + + public override bool is_empty { + get { return this.address.is_empty (); } + } + + public override string icon_name { + get { return "mark-location-symbolic"; } + } + + public Address () { + this.parameters = new Gee.HashMultiMap<string, string> (); + this.parameters["type"] = "HOME"; + } + + public Address.from_field_details (PostalAddressFieldDetails address_field) { + this.address = address_field.value; + this.parameters = address_field.parameters; + } + + /** + * Returns the TypeDescriptor that describes the type of this address + * (for example home, work, ...) + */ + public TypeDescriptor get_address_type () { + return TypeSet.general.lookup_by_parameters (this.parameters); + } + + /** + * Returns the address as a single string, with the several parts of + * the address joined together with @parts_separator. + */ + public string to_string (string parts_separator) { + string[] lines = {}; + + if (this.address.street != "") + lines += this.address.street; + if (this.address.extension != "") + lines += this.address.extension; + if (this.address.locality != "") + lines += this.address.locality; + if (this.address.region != "") + lines += this.address.region; + if (this.address.postal_code != "") + lines += this.address.postal_code; + if (this.address.po_box != "") + lines += this.address.po_box; + if (this.address.country != "") + lines += this.address.country; + if (this.address.address_format != "") + lines += this.address.address_format; + + return string.joinv (parts_separator, lines); + } + + /** + * Returns the address as a "maps:q=..." URI, which can then be used + * by supported apps to open up the specified location. + */ + public string to_maps_uri () { + var address_parts = to_string (" "); + return "maps:q=%s".printf (GLib.Uri.escape_string (address_parts)); + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new PostalAddressFieldDetails (this.address, this.parameters); + } +} diff --git a/src/core/contacts-alias-chunk.vala b/src/core/contacts-alias-chunk.vala new file mode 100644 index 0000000..921e9cf --- /dev/null +++ b/src/core/contacts-alias-chunk.vala @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +public class Contacts.AliasChunk : Chunk { + + public string alias { + get { return this._alias; } + set { + if (this._alias == value) + return; + + bool was_empty = this.is_empty; + this._alias = value; + notify_property ("alias"); + if (this.is_empty != was_empty) + notify_property ("is-empty"); + } + } + private string _alias = ""; + + public override string property_name { get { return "alias"; } } + + public override bool is_empty { get { return this._alias.strip () == ""; } } + + construct { + if (persona != null) { + return_if_fail (persona is AliasDetails); + persona.bind_property ("alias", this, "alias", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this.alias; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is AliasDetails) { + + yield ((AliasDetails) this.persona).change_alias (this.alias); + } +} diff --git a/src/core/contacts-avatar-chunk.vala b/src/core/contacts-avatar-chunk.vala new file mode 100644 index 0000000..56dbce2 --- /dev/null +++ b/src/core/contacts-avatar-chunk.vala @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +public class Contacts.AvatarChunk : Chunk { + + public LoadableIcon? avatar { + get { return this._avatar; } + set { + if (this._avatar == value) + return; + this._avatar = value; + notify_property ("avatar"); + notify_property ("is-empty"); + } + } + private LoadableIcon? _avatar = null; + + public override string property_name { get { return "avatar"; } } + + public override bool is_empty { get { return this._avatar == null; } } + + construct { + if (persona != null) { + return_if_fail (persona is AvatarDetails); + persona.bind_property ("avatar", this, "avatar", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this._avatar; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is AvatarDetails) { + yield ((AvatarDetails) this.persona).change_avatar (this.avatar); + } +} diff --git a/src/core/contacts-bin-chunk.vala b/src/core/contacts-bin-chunk.vala new file mode 100644 index 0000000..3229ddd --- /dev/null +++ b/src/core/contacts-bin-chunk.vala @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that aggregates multiple values associated to a property + * (for example, a chunk for phone numbers, or email addresses). These values + * are represented as {@link BinChunkChild}ren, which BinChunk exposes through + * the {@link GLib.ListModel} interface. + * + * One important property of BinkChunk is that it makes sure at least one empty + * child exists. This allows us to expose an immutable interface, while being + * able to synchronize with our UI (which expects this kind of behavior) + */ +public abstract class Contacts.BinChunk : Chunk, GLib.ListModel { + + private GenericArray<BinChunkChild> elements = new GenericArray<BinChunkChild> (); + + public override bool is_empty { + get { + if (this.elements.length == 0) + return true; + foreach (var chunk_element in this.elements) { + if (!chunk_element.is_empty) + return false; + } + return true; + } + } + + /** + * Should be called by subclasses when they add a child. + * It will make sure to attach the emptines check is appropriately applied. + */ + protected void add_child (BinChunkChild child) { + if (child.is_empty && has_empty_child ()) + return; + + child.notify["is-empty"].connect ((obj, pspec) => { + debug ("Child 'is-empty' changed, doing emptiness check"); + emptiness_check (); + }); + this.elements.add (child); + items_changed (this.elements.length - 1, 0, 1); + } + + /** + * Subclasses should implement this to create an empty child (which will be + * used for the emptiness check). + */ + protected abstract BinChunkChild create_empty_child (); + + // A method to check if we have at least one empty row + // if we don't, it adds an empty child + protected void emptiness_check () { + if (has_empty_child ()) + return; + + // We only have non-empty rows, add one + var child = create_empty_child (); + add_child (child); + } + + private bool has_empty_child () { + for (uint i = 0; i < this.elements.length; i++) { + if (this.elements[i].is_empty) + return true; + } + return false; + } + + public override Value? to_value () { + var afds = new Gee.HashSet<AbstractFieldDetails> (); + for (uint i = 0; i < this.elements.length; i++) { + var afd = this.elements[i].create_afd (); + if (afd != null) + afds.add (afd); + } + return (afds.size != 0)? afds : null; + } + + /** A helper function to collect the AbstractFieldDetails of the children */ + protected Gee.Set<AbstractFieldDetails> get_abstract_field_details () + requires (this.persona != null) { + var afds = new Gee.HashSet<AbstractFieldDetails> (); + for (uint i = 0; i < this.elements.length; i++) { + var afd = this.elements[i].create_afd (); + if (afd != null) + afds.add (afd); + } + + return afds; + } + + // ListModel implementation + + public uint n_items { get { return this.elements.length; } } + + public GLib.Type item_type { get { return typeof (BinChunkChild); } } + + public Object? get_item (uint i) { + if (i > this.elements.length) + return null; + return (Object) this.elements[i]; + } + + public uint get_n_items () { + return this.elements.length; + } + + public GLib.Type get_item_type () { + return typeof (BinChunkChild); + } +} + +/** + * A child of a {@link BinChunk} + */ +public abstract class Contacts.BinChunkChild : GLib.Object { + + public Gee.MultiMap<string, string> parameters { get; set; } + + /** + * Whether this BinChunkChild is empty. You can use the notify signal to + * listen for changes. + */ + public abstract bool is_empty { get; } + + /** + * The icon name that best represents this BinChunkChild + */ + public abstract string icon_name { get; } + + /** + * Creates an AbstractFieldDetails from the contents of this child + * + * If the contents are invalid (or empty), it returns null. + */ + public abstract AbstractFieldDetails? create_afd (); + + // A helper to change a string field with the proper propery notifies + protected void change_string_prop (string prop_name, + ref string old_value, + string new_value) { + if (new_value == old_value) + return; + + bool notify_empty = ((new_value.strip () == "") != (old_value.strip () == "")); + // Don't strip value when setting the old one, since we don't want to + // prevent users from entering a space or a newline :D + old_value = new_value; + notify_property (prop_name); + if (notify_empty) + notify_property ("is-empty"); + } +} diff --git a/src/core/contacts-birthday-chunk.vala b/src/core/contacts-birthday-chunk.vala new file mode 100644 index 0000000..087da6a --- /dev/null +++ b/src/core/contacts-birthday-chunk.vala @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the birthday of a contact (similar to + * {@link Folks.BirthdayDetails}}. + */ +public class Contacts.BirthdayChunk : Chunk { + + public DateTime? birthday { + get { return this._birthday; } + set { + if (this._birthday == null && value == null) + return; + + if (this._birthday != null && value != null + && this._birthday.equal (value.to_utc ())) + return; + + this._birthday = (value != null)? value.to_utc () : null; + notify_property ("birthday"); + notify_property ("is-empty"); + } + } + private DateTime? _birthday = null; + + public override string property_name { get { return "birthday"; } } + + public override bool is_empty { get { return this.birthday == null; } } + + construct { + if (persona != null) { + return_if_fail (persona is BirthdayDetails); + persona.bind_property ("birthday", this, "birthday", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this.birthday; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is BirthdayDetails) { + yield ((BirthdayDetails) this.persona).change_birthday (this.birthday); + } +} diff --git a/src/core/contacts-chunk.vala b/src/core/contacts-chunk.vala new file mode 100644 index 0000000..998ad69 --- /dev/null +++ b/src/core/contacts-chunk.vala @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A "chunk" is a piece of data that describes a specific property of a + * {@link Contact}. Each chunk usually maps to a specific vCard property, or an + * interface related to a property of a {@link Folks.Persona}. + */ +public abstract class Contacts.Chunk : GLib.Object { + + /** The associated persona (or null if we're creating a new one) */ + public Persona? persona { get; construct set; default = null; } + + /** + * The specific property of this chunk. + * + * Note that this should match with the string representation of a + * {@link Folks.PersonaDetail}. + */ + public abstract string property_name { get; } + + /** + * Whether this is empty. As an example, you can use to changes in this + * property to update any UI. + */ + 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. + */ + public bool changed { get; protected set; default = false; } + + /** + * Converts this chunk into a GLib.Value, as expected by API like + * {@link Folks.PersonaStore.add_persona_from_details} + * + * If the field is empty or non-existent, it should return null. + */ + public abstract Value? to_value (); + + /** + * Calls the appropriate API to save to the persona. + */ + public abstract async void save_to_persona () throws GLib.Error + requires (this.persona != null); +} diff --git a/src/core/contacts-contact.vala b/src/core/contacts-contact.vala new file mode 100644 index 0000000..889284f --- /dev/null +++ b/src/core/contacts-contact.vala @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A Contact is an object that represents a data model around a set of + * contact properties. This can either come from a {@link Folks.Individual}, an + * empty set (when creating contacts) or a different data source (like a vCard). + * + * Since the classes Folks provides assume valid data, we can't/shouldn't + * really use them (for example, a PostalAddresFieldDetails does not allow + * empty addresses), so that is another easy use for this separate class. + */ +public class Contacts.Contact : GLib.Object, GLib.ListModel { + + private GenericArray<Chunk> chunks = new GenericArray<Chunk> (); + + /** The underlying individual, if any */ + public unowned Individual? individual { get; construct set; default = null; } + + public unowned Store contacts_store { get; construct set; } + + /** Similar to fetch_display_name(), but never returns null */ + public string display_name { + owned get { return fetch_display_name () ?? _("Unnamed Person"); } + } + + construct { + if (this.individual != null) { + this.individual.personas_changed.connect (on_individual_personas_changed); + on_individual_personas_changed (this.individual, + this.individual.personas, + Gee.Set.empty<Persona> ()); + } else { + // At the very least let's add an empty full-name chunk + create_chunk ("full-name", null); + } + } + + /** Creates a Contact that acts as a wrapper around an Individual */ + public Contact.for_individual (Individual individual, Store contacts_store) { + Object (individual: individual, contacts_store: contacts_store); + } + + /** Creates a new empty contact */ + public Contact.for_new (Store contacts_store) { + Object (individual: null, contacts_store: contacts_store); + } + + private void on_individual_personas_changed (Individual individual, + Gee.Set<Persona> added, + Gee.Set<Persona> removed) { + uint old_size = this.chunks.length; + foreach (var persona in added) + add_persona (persona); + items_changed (old_size - 1, 0, this.chunks.length - old_size); + + foreach (var persona in removed) { + for (uint i = 0; i < this.chunks.length; i++) { + if (this.chunks[i].persona == persona) { + this.chunks.remove_index (i); + items_changed (i, 1, 0); + i--; + } + } + } + } + + private void add_persona (Persona persona) { + if (persona is AliasDetails) + create_chunk_internal ("alias", persona); + if (persona is AvatarDetails) + create_chunk_internal ("avatar", persona); + if (persona is BirthdayDetails) + create_chunk_internal ("birthday", persona); + if (persona is EmailDetails) + create_chunk_internal ("email-addresses", persona); + if (persona is ImDetails) + create_chunk_internal ("im-addresses", persona); + if (persona is NameDetails) { + create_chunk_internal ("full-name", persona); + create_chunk_internal ("structured-name", persona); + create_chunk_internal ("nickname", persona); + } + if (persona is NoteDetails) + create_chunk_internal ("notes", persona); + if (persona is PhoneDetails) + create_chunk_internal ("phone-numbers", persona); + if (persona is PostalAddressDetails) + create_chunk_internal ("postal-addresses", persona); + if (persona is RoleDetails) + create_chunk_internal ("roles", persona); + if (persona is UrlDetails) + create_chunk_internal ("urls", persona); + } + + public unowned Chunk? create_chunk (string property_name, Persona? persona) { + var pos = create_chunk_internal (property_name, persona); + if (pos == Gtk.INVALID_LIST_POSITION) + return null; + items_changed (pos, 0, 1); + return this.chunks[pos]; + } + + // Helper to create a chunk and return its position, without items_changed() + private uint create_chunk_internal (string property_name, Persona? persona) { + var chunk_gtype = chunk_gtype_for_property (property_name); + if (chunk_gtype == GLib.Type.NONE) { + debug ("unsupported property '%s', ignoring", property_name); + return Gtk.INVALID_LIST_POSITION; + } + + var chunk = (Chunk) Object.new (chunk_gtype, + "persona", persona, + null); + this.chunks.add (chunk); + return this.chunks.length - 1; + } + + private GLib.Type chunk_gtype_for_property (string property_name) { + switch (property_name) { // Please keep these sorted + case "alias": + return typeof (AliasChunk); + case "avatar": + return typeof (AvatarChunk); + case "birthday": + return typeof (BirthdayChunk); + case "email-addresses": + return typeof (EmailAddressesChunk); + case "full-name": + return typeof (FullNameChunk); + case "im-addresses": + return typeof (ImAddressesChunk); + case "nickname": + return typeof (NicknameChunk); + case "notes": + return typeof (NotesChunk); + case "phone-numbers": + return typeof (PhonesChunk); + case "postal-addresses": + return typeof (AddressesChunk); + case "roles": + return typeof (RolesChunk); + case "structured-name": + return typeof (StructuredNameChunk); + case "urls": + return typeof (UrlsChunk); + } + + return GLib.Type.NONE; + } + + /** + * Tries to get the name for the contact by iterating over the chunks that + * represent some form of name. If none is found, it returns null. + */ + public string? fetch_name () { + var alias_chunk = get_most_relevant_chunk ("alias"); + if (alias_chunk != null) + return ((AliasChunk) alias_chunk).alias; + + var fn_chunk = get_most_relevant_chunk ("full-name"); + if (fn_chunk != null) + return ((FullNameChunk) fn_chunk).full_name; + + var sn_chunk = get_most_relevant_chunk ("structured-name"); + if (sn_chunk != null) + return ((StructuredNameChunk) sn_chunk).structured_name.to_string (); + + var nick_chunk = get_most_relevant_chunk ("nickname"); + if (nick_chunk != null) + return ((NicknameChunk) nick_chunk).nickname; + + return null; + } + + /** + * Tries to get the displayable name for the contact. Similar to fetch_name, + * but also checks for fields that are not a name, but might still represent + * a contact (for example an email address) + */ + public string? fetch_display_name () { + var name = fetch_name (); + if (name != null) + return name; + + var emails_chunk = get_most_relevant_chunk ("email-addresses"); + if (emails_chunk != null) { + var email = ((EmailAddressesChunk) emails_chunk).get_item (0); + return ((EmailAddress) email).raw_address; + } + + var phones_chunk = get_most_relevant_chunk ("phone-numbers"); + if (phones_chunk != null) { + var phone = ((PhonesChunk) phones_chunk).get_item (0); + return ((Phone) phone).raw_number; + } + + return null; + } + + /** + * A helper function to return the {@link Chunk} that best represents the + * property of the contact (or null if none). + */ + public Chunk? get_most_relevant_chunk (string property_name, bool allow_empty = false) { + var filter = new ChunkFilter.for_property (property_name); + filter.allow_empty = allow_empty; + var chunks = new Gtk.FilterListModel (this, (owned) filter); + + // From these chunks, select the one from the primary store. If there's + // none, just select the first one + unowned var primary_store = this.contacts_store.aggregator.primary_store; + for (uint i = 0; i < chunks.get_n_items (); i++) { + var chunk = (Chunk) chunks.get_item (i); + if (chunk.persona != null && chunk.persona.store == primary_store) + return chunk; + } + return (Chunk?) chunks.get_item (0); + } + + public Object? get_item (uint i) { + if (i > this.chunks.length) + return null; + return this.chunks[i]; + } + + public uint get_n_items () { + return this.chunks.length; + } + + public GLib.Type get_item_type () { + return typeof (Chunk); + } + + /** + * 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. + */ + public async void apply_changes () throws GLib.Error { + // 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 (!(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); + // TODO: maybe add a fallback to save to a different persona? + // We could maybe store it and add it to a new one, but that might make + // properties overlap + continue; + } + + debug ("Saving property '%s' to persona %s", + chunk.property_name, chunk.persona.uid); + yield chunk.save_to_persona (); + debug ("Saved property '%s' to persona %s", + chunk.property_name, chunk.persona.uid); + } + + // 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]; + if (chunk.persona != null) + continue; + + var value = chunk.to_value (); + if (value == null // Skip empty properties + || value.peek_pointer () == null) // ugh, Vala + continue; + + if (chunk.property_name in new_details) + warning ("Got multiple chunks for property '%s'", chunk.property_name); + new_details.insert (chunk.property_name, (owned) value); + } + if (new_details.size () != 0) { + debug ("Creating new persona with %u properties", new_details.size ()); + unowned var primary_store = this.contacts_store.aggregator.primary_store; + return_if_fail (primary_store != null); + var persona = yield primary_store.add_persona_from_details (new_details); + debug ("Successfully created new persona %p", persona); + // FIXME: should we set the persona for these chunks? + } + } +} diff --git a/src/core/contacts-email-addresses-chunk.vala b/src/core/contacts-email-addresses-chunk.vala new file mode 100644 index 0000000..1119a2c --- /dev/null +++ b/src/core/contacts-email-addresses-chunk.vala @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +public class Contacts.EmailAddressesChunk : BinChunk { + + public override string property_name { get { return "email-addresses"; } } + + construct { + if (persona != null) { + return_if_fail (persona is EmailDetails); + unowned var email_details = (EmailDetails) persona; + + foreach (var email_field in email_details.email_addresses) { + var email = new EmailAddress.from_field_details (email_field); + add_child (email); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new EmailAddress (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is EmailDetails) { + var afds = (Gee.Set<EmailFieldDetails>) get_abstract_field_details (); + yield ((EmailDetails) this.persona).change_email_addresses (afds); + } +} + +public class Contacts.EmailAddress : BinChunkChild { + + public string raw_address { + get { return this._raw_address; } + set { change_string_prop ("raw-address", ref this._raw_address, value); } + } + private string _raw_address = ""; + + public override bool is_empty { + get { return this.raw_address.strip () == ""; } + } + + public override string icon_name { + get { return "mail-unread-symbolic"; } + } + + public EmailAddress () { + this.parameters = new Gee.HashMultiMap<string, string> (); + this.parameters["type"] = "PERSONAL"; + } + + public EmailAddress.from_field_details (EmailFieldDetails email_field) { + this.raw_address = email_field.value; + this.parameters = email_field.parameters; + } + + /** + * Returns the TypeDescriptor that describes the type of the email address + * (for example personal, work, ...) + */ + public TypeDescriptor get_email_address_type () { + return TypeSet.email.lookup_by_parameters (this.parameters); + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new EmailFieldDetails (this.raw_address, this.parameters); + } + + 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 new file mode 100644 index 0000000..647f556 --- /dev/null +++ b/src/core/contacts-full-name-chunk.vala @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the full name of a contact as a single + * string (contrary to the structured name, where the name is split up in the + * several constituent parts}. + */ +public class Contacts.FullNameChunk : Chunk { + + public string full_name { + get { return this._full_name; } + set { + if (this._full_name == value) + return; + + bool was_empty = this.is_empty; + this._full_name = value; + notify_property ("full-name"); + if (this.is_empty != was_empty) + notify_property ("is-empty"); + } + } + private string _full_name = ""; + + public override string property_name { get { return "full-name"; } } + + public override bool is_empty { get { return this._full_name.strip () == ""; } } + + construct { + if (persona != null) { + return_if_fail (persona is NameDetails); + persona.bind_property ("full-name", this, "full-name", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this.full_name; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is NameDetails) { + yield ((NameDetails) this.persona).change_full_name (this.full_name); + } +} diff --git a/src/core/contacts-im-addresses-chunk.vala b/src/core/contacts-im-addresses-chunk.vala new file mode 100644 index 0000000..031f804 --- /dev/null +++ b/src/core/contacts-im-addresses-chunk.vala @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the internet messaging (IM) addresses of a + * contact (similar to {@link Folks.ImDetails}}. Each element is a + * {@link ImAddress}. + */ +public class Contacts.ImAddressesChunk : BinChunk { + + public override string property_name { get { return "im-addresses"; } } + + construct { + if (persona != null) { + return_if_fail (persona is ImDetails); + unowned var im_details = (ImDetails) persona; + + var iter = im_details.im_addresses.map_iterator (); + while (iter.next ()) { + var protocol = iter.get_key (); + var im = new ImAddress.from_field_details (iter.get_value (), protocol); + add_child (im); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new ImAddress (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is ImDetails) { + // We can't use get_abstract_field_details() here, since we need the + // protocol as well, and to use a Gee.MultiMap for it + var afds = new Gee.HashMultiMap<string, ImFieldDetails> (); + for (uint i = 0; i < get_n_items (); i++) { + var im_addr = (ImAddress) get_item (i); + var afd = (ImFieldDetails) im_addr.create_afd (); + if (afd != null) + afds[im_addr.protocol] = afd; + } + + yield ((ImDetails) this.persona).change_im_addresses (afds); + } +} + +public class Contacts.ImAddress : BinChunkChild { + + public string protocol { get; private set; default = ""; } + + public string address { + get { return this._address; } + set { change_string_prop ("address", ref this._address, value); } + } + private string _address = ""; + + public override bool is_empty { + get { return this.address.strip () == ""; } + } + + public override string icon_name { + get { return "chat-symbolic"; } + } + + public ImAddress () { + this.parameters = new Gee.HashMultiMap<string, string> (); + } + + public ImAddress.from_field_details (ImFieldDetails im_field, string protocol) { + this.address = im_field.value; + this.protocol = protocol; + this.parameters = im_field.parameters; + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new ImFieldDetails (this.address, this.parameters); + } +} diff --git a/src/core/contacts-nickname-chunk.vala b/src/core/contacts-nickname-chunk.vala new file mode 100644 index 0000000..ba505f0 --- /dev/null +++ b/src/core/contacts-nickname-chunk.vala @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the nickname of a contact. + */ +public class Contacts.NicknameChunk : Chunk { + + public string nickname { + get { return this._nickname; } + set { + if (this._nickname == value) + return; + + bool was_empty = this.is_empty; + this._nickname = value; + notify_property ("nickname"); + if (this.is_empty != was_empty) + notify_property ("is-empty"); + } + } + private string _nickname = ""; + + public override string property_name { get { return "nickname"; } } + + public override bool is_empty { get { return this._nickname.strip () == ""; } } + + construct { + if (persona != null) { + return_if_fail (persona is NameDetails); + persona.bind_property ("nickname", this, "nickname", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this.nickname; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is NameDetails) { + + yield ((NameDetails) this.persona).change_nickname (this.nickname); + } +} diff --git a/src/core/contacts-notes-chunk.vala b/src/core/contacts-notes-chunk.vala new file mode 100644 index 0000000..45b5c43 --- /dev/null +++ b/src/core/contacts-notes-chunk.vala @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the freeform notes attached to a contact + * (similar to {@link Folks.NoteDetails}}. Each element is a {@link Note}. + */ +public class Contacts.NotesChunk : BinChunk { + + public override string property_name { get { return "notes"; } } + + construct { + if (persona != null) { + return_if_fail (persona is NoteDetails); + unowned var note_details = (NoteDetails) persona; + + foreach (var note_field in note_details.notes) { + var note = new Note.from_field_details (note_field); + add_child (note); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new Note (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is PhoneDetails) { + var afds = (Gee.Set<NoteFieldDetails>) get_abstract_field_details (); + yield ((NoteDetails) this.persona).change_notes (afds); + } +} + +public class Contacts.Note : BinChunkChild { + + public string text { + get { return this._text; } + set { change_string_prop ("text", ref this._text, value); } + } + private string _text = ""; + + public override bool is_empty { + get { return this.text.strip () == ""; } + } + + public override string icon_name { + get { return "note-symbolic"; } + } + + public Note () { + this.parameters = new Gee.HashMultiMap<string, string> (); + this.parameters["type"] = "PERSONAL"; + } + + public Note.from_field_details (NoteFieldDetails note_field) { + this.text = note_field.value; + this.parameters = note_field.parameters; + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new NoteFieldDetails (this.text, this.parameters); + } +} diff --git a/src/core/contacts-phones-chunk.vala b/src/core/contacts-phones-chunk.vala new file mode 100644 index 0000000..8135d98 --- /dev/null +++ b/src/core/contacts-phones-chunk.vala @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the phone numbers of a contact (similar to + * {@link Folks.PhoneDetails}}. Each element is a {@link Phone}. + */ +public class Contacts.PhonesChunk : BinChunk { + + public override string property_name { get { return "phone-numbers"; } } + + construct { + if (persona != null) { + return_if_fail (persona is PhoneDetails); + unowned var phone_details = (PhoneDetails) persona; + + foreach (var phone_field in phone_details.phone_numbers) { + var phone = new Phone.from_field_details (phone_field); + add_child (phone); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new Phone (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is PhoneDetails) { + var afds = (Gee.Set<PhoneFieldDetails>) get_abstract_field_details (); + yield ((PhoneDetails) this.persona).change_phone_numbers (afds); + } +} + +public class Contacts.Phone : BinChunkChild { + + /** + * The "raw" phone number as inputted by a user or from a contact. It may or + * may not be an actual valid phone number. + */ + public string raw_number { + get { return this._raw_number; } + set { change_string_prop ("raw-number", ref this._raw_number, value); } + } + private string _raw_number = ""; + + public override bool is_empty { + get { return this.raw_number.strip () == ""; } + } + + public override string icon_name { + get { return "phone-symbolic"; } + } + + public Phone () { + this.parameters = new Gee.HashMultiMap<string, string> (); + this.parameters["type"] = "CELL"; + } + + public Phone.from_field_details (PhoneFieldDetails phone_field) { + this.raw_number = phone_field.value; + this.parameters = phone_field.parameters; + } + + /** + * Returns the TypeDescriptor that describes the type of phone number + * (for example mobile, work, fax, ...) + */ + public TypeDescriptor get_phone_type () { + return TypeSet.phone.lookup_by_parameters (this.parameters); + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new PhoneFieldDetails (this.raw_number, this.parameters); + } +} diff --git a/src/core/contacts-roles-chunk.vala b/src/core/contacts-roles-chunk.vala new file mode 100644 index 0000000..bec585b --- /dev/null +++ b/src/core/contacts-roles-chunk.vala @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the organizations and/or roles of a contact + * (similar to {@link Folks.RoleDetails}}. Each element is a + * {@link Contacts.OrgRole}. + */ +public class Contacts.RolesChunk : BinChunk { + + public override string property_name { get { return "roles"; } } + + construct { + if (persona != null) { + return_if_fail (persona is RoleDetails); + unowned var role_details = (RoleDetails) persona; + + foreach (var role_field in role_details.roles) { + var role = new OrgRole.from_field_details (role_field); + add_child (role); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new OrgRole (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is RoleDetails) { + var afds = (Gee.Set<RoleFieldDetails>) get_abstract_field_details (); + yield ((RoleDetails) this.persona).change_roles (afds); + } +} + +public class Contacts.OrgRole : BinChunkChild { + + public Role role { get; private set; default = new Role (); } + + public override bool is_empty { + get { return this.role.is_empty (); } + } + + public override string icon_name { + get { return "building-symbolic"; } + } + + public OrgRole () { + this.parameters = new Gee.HashMultiMap<string, string> (); + } + + public OrgRole.from_field_details (RoleFieldDetails role_field) { + this.role = role_field.value; + this.parameters = role_field.parameters; + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new RoleFieldDetails (this.role, this.parameters); + } + + public string to_string () { + if (this.role.title != "") { + if (this.role.organisation_name != "") { + // TRANSLATORS: "$ROLE at $ORGANISATION", e.g. "CEO at Linux Inc." + return _("%s at %s").printf (this.role.title, this.role.organisation_name); + } + + return this.role.title; + } + + return this.role.organisation_name; + } +} diff --git a/src/core/contacts-structured-name-chunk.vala b/src/core/contacts-structured-name-chunk.vala new file mode 100644 index 0000000..07cbc8f --- /dev/null +++ b/src/core/contacts-structured-name-chunk.vala @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the structured name of a contact. + * + * The structured represents a full name split in its constituent parts (given + * name, family name, etc.) + */ +public class Contacts.StructuredNameChunk : Chunk { + + public StructuredName structured_name { + get { return this._structured_name; } + set { + if (this._structured_name == value) + return; + if (this._structured_name != null && value != null + && this._structured_name.equal (value)) + return; + + bool was_empty = this.is_empty; + this._structured_name = value; + notify_property ("structured-name"); + if (this.is_empty != was_empty) + notify_property ("is-empty"); + } + } + private StructuredName _structured_name = new StructuredName.simple (null, null); + + public override string property_name { get { return "structured-name"; } } + + public override bool is_empty { + get { + return this._structured_name == null || this._structured_name.is_empty (); + } + } + + construct { + if (persona != null) { + return_if_fail (persona is NameDetails); + persona.bind_property ("structured-name", this, "structured-name", BindingFlags.SYNC_CREATE); + } + } + + public override Value? to_value () { + return this.structured_name; + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is NameDetails) { + yield ((NameDetails) this.persona).change_structured_name (this.structured_name); + } +} diff --git a/src/core/contacts-urls-chunk.vala b/src/core/contacts-urls-chunk.vala new file mode 100644 index 0000000..671fc4d --- /dev/null +++ b/src/core/contacts-urls-chunk.vala @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Niels De Graef <nielsdegraef@gmail.com> + * + * 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 2 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/>. + */ + +using Folks; + +/** + * A {@link Chunk} that represents the associated URLs of a contact (similar to + * {@link Folks.UrlDetails}}. Each element is a {@link Contacts.Url}. + */ +public class Contacts.UrlsChunk : BinChunk { + + public override string property_name { get { return "urls"; } } + + construct { + if (persona != null) { + return_if_fail (persona is UrlDetails); + unowned var url_details = (UrlDetails) persona; + + foreach (var url_field in url_details.urls) { + var url = new Url.from_field_details (url_field); + add_child (url); + } + } + + emptiness_check (); + } + + protected override BinChunkChild create_empty_child () { + return new Url (); + } + + public override async void save_to_persona () throws GLib.Error + requires (this.persona is UrlDetails) { + var afds = (Gee.Set<UrlFieldDetails>) get_abstract_field_details (); + yield ((UrlDetails) this.persona).change_urls (afds); + } +} + +public class Contacts.Url : BinChunkChild { + + public string raw_url { + get { return this._raw_url; } + set { change_string_prop ("raw-url", ref this._raw_url, value); } + } + private string _raw_url = ""; + + public override bool is_empty { + get { return this.raw_url.strip () == ""; } + } + + public override string icon_name { + get { return "website-symbolic"; } + } + + public Url () { + this.parameters = new Gee.HashMultiMap<string, string> (); + this.parameters["type"] = "PERSONAL"; + } + + public Url.from_field_details (UrlFieldDetails url_field) { + this.raw_url = url_field.value; + this.parameters = url_field.parameters; + } + + /** + * Tries to return an absolute URL (with a scheme). + * Since we know contact URL values are for web addresses, we try to fall + * back to https if there is no known scheme + */ + public string get_absolute_url () { + string scheme = Uri.parse_scheme (this.raw_url); + return (scheme != null)? this.raw_url : "https://" + this.raw_url; + } + + public override AbstractFieldDetails? create_afd () { + if (this.is_empty) + return null; + + return new UrlFieldDetails (this.raw_url, this.parameters); + } +} |