From 19c9893ecf4a3740f67c58333348f0e9245ecd42 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 20 Jan 2021 15:13:52 +0100 Subject: [PATCH] Contacts: Add contacts list --- po/POTFILES.in | 2 + src/calls-contacts-box.c | 218 +++++++++++++++++++++++++++++++++++++++ src/calls-contacts-box.h | 40 +++++++ src/calls-contacts-row.c | 207 +++++++++++++++++++++++++++++++++++++ src/calls-contacts-row.h | 27 +++++ src/calls-main-window.c | 11 +- src/calls.gresources.xml | 2 + src/meson.build | 2 + src/style.css | 26 +---- src/ui/contacts-box.ui | 85 +++++++++++++++ src/ui/contacts-row.ui | 48 +++++++++ src/ui/main-window.ui | 12 +-- 12 files changed, 645 insertions(+), 35 deletions(-) create mode 100644 src/calls-contacts-box.c create mode 100644 src/calls-contacts-box.h create mode 100644 src/calls-contacts-row.c create mode 100644 src/calls-contacts-row.h create mode 100644 src/ui/contacts-box.ui create mode 100644 src/ui/contacts-row.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index ac35959..857faa7 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -8,7 +8,9 @@ src/calls-call-record.c src/calls-call-record-row.c src/calls-call-selector-item.c src/calls-call-window.c +src/calls-contacts-box.c src/calls-contacts-provider.c +src/calls-contacts-row.c src/calls-encryption-indicator.c src/calls-history-box.c src/calls-in-app-notification.c diff --git a/src/calls-contacts-box.c b/src/calls-contacts-box.c new file mode 100644 index 0000000..3acf4a3 --- /dev/null +++ b/src/calls-contacts-box.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls 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 Calls. If not, see . + * + * Author: Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "calls-manager.h" +#include "calls-contacts-provider.h" +#include "calls-contacts-box.h" +#include "calls-contacts-row.h" + +#include + +#define HANDY_USE_UNSTABLE_API +#include + +struct _CallsContactsBox +{ + GtkBin parent_instance; + + GtkWidget *search_entry; + GtkWidget *contacts_frame; + GtkWidget *contacts_listbox; + GtkWidget *placeholder_empty; + + FolksSimpleQuery *search_query; +}; + +G_DEFINE_TYPE (CallsContactsBox, calls_contacts_box, GTK_TYPE_BIN); + +static void +search_changed_cb (CallsContactsBox *self, + GtkEntry *entry) +{ + const gchar *search_text; + search_text = gtk_entry_get_text (entry); + + folks_simple_query_set_query_string (self->search_query, search_text); + + gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->contacts_listbox)); +} + +static gboolean +contacts_filter_func (CallsContactsRow *row, + CallsContactsBox *self) +{ + FolksIndividual *item = calls_contacts_row_get_item (row); + + return folks_query_is_match (FOLKS_QUERY (self->search_query), item) > 0; +} + + +static void +adjust_style (CallsContactsBox *self, GtkWidget *widget) +{ + g_return_if_fail (CALLS_IS_CONTACTS_BOX (self)); + + if (gtk_widget_get_mapped (widget)) + { + gtk_frame_set_shadow_type (GTK_FRAME (self->contacts_frame), GTK_SHADOW_NONE); + gtk_widget_set_vexpand (self->contacts_frame, TRUE); + gtk_style_context_add_class (gtk_widget_get_style_context (self->contacts_listbox), + "no-background"); + } + else + { + gtk_frame_set_shadow_type (GTK_FRAME (self->contacts_frame), GTK_SHADOW_ETCHED_IN); + gtk_widget_set_vexpand (self->contacts_frame, FALSE); + gtk_style_context_remove_class (gtk_widget_get_style_context (self->contacts_listbox), + "no-background"); + } +} + +static void +header_cb (GtkListBoxRow *row, + GtkListBoxRow *before) +{ + if (!before) + { + gtk_list_box_row_set_header (row, NULL); + return; + } + + + if (!gtk_list_box_row_get_header (row)) + gtk_list_box_row_set_header (row, gtk_separator_new (GTK_ORIENTATION_HORIZONTAL)); +} + +static void +contacts_provider_added (CallsContactsBox *self, + FolksIndividual *individual) +{ + GtkWidget *row; + row = calls_contacts_row_new (individual); + + gtk_container_add (GTK_CONTAINER (self->contacts_listbox), row); +} + +static void +contacts_provider_removed (CallsContactsBox *self, + FolksIndividual *individual) +{ + g_autoptr (GList) list = gtk_container_get_children (GTK_CONTAINER (self->contacts_listbox)); + GList *l; + + for (l = list; l != NULL; l = l->next) { + CallsContactsRow *row = CALLS_CONTACTS_ROW (l->data); + if (calls_contacts_row_get_item (row) == individual) + gtk_container_remove (GTK_CONTAINER (self->contacts_listbox), GTK_WIDGET (row)); + } +} + +static gint +contacts_sort_func (CallsContactsRow *a, + CallsContactsRow *b, + gpointer user_data) +{ + const gchar *name_a = folks_individual_get_display_name (calls_contacts_row_get_item (a)); + const gchar *name_b = folks_individual_get_display_name (calls_contacts_row_get_item (b)); + + return g_strcmp0 (name_a, name_b); +} + + +static void +calls_contacts_box_class_init (CallsContactsBoxClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/contacts-box.ui"); + gtk_widget_class_bind_template_child (widget_class, CallsContactsBox, contacts_listbox); + gtk_widget_class_bind_template_child (widget_class, CallsContactsBox, contacts_frame); + gtk_widget_class_bind_template_child (widget_class, CallsContactsBox, search_entry); + gtk_widget_class_bind_template_child (widget_class, CallsContactsBox, placeholder_empty); +} + + +static void +calls_contacts_box_init (CallsContactsBox *self) +{ + CallsContactsProvider *contacts_provider; + g_autoptr (GeeCollection) individuals = NULL; + gchar* query_fields[] = { "alias", + "full-name", + "nickname", + "structured-name", + "phone-numbers" }; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->search_query = folks_simple_query_new ("", query_fields, 5); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->contacts_listbox), + (GtkListBoxUpdateHeaderFunc) header_cb, + NULL, + NULL); + + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->contacts_listbox), + (GtkListBoxSortFunc) contacts_sort_func, + NULL, + NULL); + + gtk_list_box_set_filter_func (GTK_LIST_BOX (self->contacts_listbox), + (GtkListBoxFilterFunc) contacts_filter_func, + self, + NULL); + + g_signal_connect_swapped (self->placeholder_empty, "map", G_CALLBACK (adjust_style), self); + g_signal_connect_swapped (self->placeholder_empty, "unmap", G_CALLBACK (adjust_style), self); + + contacts_provider = calls_manager_get_contacts_provider (calls_manager_get_default ()); + individuals = calls_contacts_provider_get_individuals (contacts_provider); + + g_signal_connect_swapped (contacts_provider, + "added", + G_CALLBACK (contacts_provider_added), + self); + + g_signal_connect_swapped (contacts_provider, + "removed", + G_CALLBACK (contacts_provider_removed), + self); + + g_signal_connect_swapped (self->search_entry, + "search-changed", + G_CALLBACK (search_changed_cb), + self); + + if (!gee_collection_get_is_empty (individuals)) + calls_contacts_provider_consume_iter_on_idle (gee_iterable_iterator (GEE_ITERABLE (individuals)), + (IdleCallback) contacts_provider_added, + self); +} + + +GtkWidget * +calls_contacts_box_new () +{ + return g_object_new (CALLS_TYPE_CONTACTS_BOX, NULL); +} diff --git a/src/calls-contacts-box.h b/src/calls-contacts-box.h new file mode 100644 index 0000000..9f1616d --- /dev/null +++ b/src/calls-contacts-box.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls 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 Calls. If not, see . + * + * Author: Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#ifndef CALLS_CONTACTS_BOX_H__ +#define CALLS_CONTACTS_BOX_H__ + +#include + +G_BEGIN_DECLS + +#define CALLS_TYPE_CONTACTS_BOX (calls_contacts_box_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsContactsBox, calls_contacts_box, CALLS, CONTACTS_BOX, GtkBin); + +GtkWidget * calls_contacts_box_new (void); + +G_END_DECLS + +#endif /* CALLS_CONTACTS_BOX_H__ */ diff --git a/src/calls-contacts-row.c b/src/calls-contacts-row.c new file mode 100644 index 0000000..46ac7c5 --- /dev/null +++ b/src/calls-contacts-row.c @@ -0,0 +1,207 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* calls-contacts-row.c + * + * Copyright 2020 Purism SPC + * + * Author(s): + * Mohammed Sadiq + * Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#include "calls-contacts-row.h" +#include "calls-contacts-provider.h" + +struct _CallsContactsRow +{ + GtkListBoxRow parent_instance; + + GtkWidget *avatar; + GtkWidget *title; + GtkWidget *grid; + + gint n_phonenumbers; + + FolksIndividual *item; +}; + +G_DEFINE_TYPE (CallsContactsRow, calls_contacts_row, GTK_TYPE_LIST_BOX_ROW) + + +static GdkPixbuf * +calls_contacts_row_set_pixbuf (gint size, + gpointer item) +{ + g_autoptr (GError) error = NULL; + GLoadableIcon *icon; + g_autoptr (GInputStream) stream = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + + g_return_val_if_fail (FOLKS_IS_INDIVIDUAL (item), NULL); + + FolksAvatarDetails *avatar_details = FOLKS_AVATAR_DETAILS (item); + + if (avatar_details == NULL) + return NULL; + + icon = folks_avatar_details_get_avatar (avatar_details); + + if (icon == NULL) + return NULL; + + stream = g_loadable_icon_load (icon, size, NULL, NULL, &error); + + if (error) + return NULL; + + pixbuf = gdk_pixbuf_new_from_stream_at_scale (stream, size, size, TRUE, NULL, &error); + + if (error) + return NULL; + + return g_steal_pointer (&pixbuf); +} + +static void +insert_phonenumber (CallsContactsRow *self, + const gchar *number) +{ + GtkWidget *label = gtk_label_new (number); + GtkWidget *button = gtk_button_new_from_icon_name ("call-start-symbolic", GTK_ICON_SIZE_BUTTON); + + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_widget_show (label); + gtk_grid_attach (GTK_GRID (self->grid), label, 1, self->n_phonenumbers, 1, 1); + + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_actionable_set_action_name (GTK_ACTIONABLE (button), "app.dial"); + gtk_actionable_set_action_target (GTK_ACTIONABLE (button), "s", number, NULL); + gtk_widget_show (button); + gtk_grid_attach_next_to (GTK_GRID (self->grid), + button, + label, + GTK_POS_RIGHT, + 1, + 1); + + self->n_phonenumbers++; +} + +static void +phone_numbers_changed_cb (CallsContactsRow *self) +{ + GeeIterator *phone_iter; + g_autoptr (GeeSet) phone_numbers; + + while (gtk_grid_get_child_at (GTK_GRID (self->grid), 1, 1) != NULL) { + gtk_grid_remove_row (GTK_GRID (self->grid), 1); + } + + self->n_phonenumbers = 1; + g_object_get (self->item, "phone-numbers", &phone_numbers, NULL); + phone_iter = gee_iterable_iterator (GEE_ITERABLE (phone_numbers)); + + while (gee_iterator_next (phone_iter)) { + // FIXME: We can't use g_autoptr because it's not implemented in the folks version in debian + FolksAbstractFieldDetails *detail = gee_iterator_get (phone_iter); + + if (FOLKS_IS_PHONE_FIELD_DETAILS (detail)) { + FolksPhoneFieldDetails *phone = FOLKS_PHONE_FIELD_DETAILS (detail); + g_autofree gchar *number = NULL; + number = folks_phone_field_details_get_normalised (phone); + if (number) + insert_phonenumber (self, number); + } + g_object_unref (detail); + } +} + +static void +avatar_changed_cb (CallsContactsRow *self) +{ + // TODO: Load avatar async once https://gitlab.gnome.org/GNOME/libhandy/-/merge_requests/637 is merged + hdy_avatar_set_image_load_func (HDY_AVATAR (self->avatar), + calls_contacts_row_set_pixbuf, + g_object_ref (self->item), g_object_unref); +} + +static void +calls_contacts_row_dispose (GObject *object) +{ + CallsContactsRow *self = CALLS_CONTACTS_ROW (object); + + g_clear_object (&self->item); + + G_OBJECT_CLASS (calls_contacts_row_parent_class)->dispose (object); +} + +static void +calls_contacts_row_class_init (CallsContactsRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = calls_contacts_row_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/calls/" + "ui/contacts-row.ui"); + gtk_widget_class_bind_template_child (widget_class, CallsContactsRow, avatar); + gtk_widget_class_bind_template_child (widget_class, CallsContactsRow, title); + gtk_widget_class_bind_template_child (widget_class, CallsContactsRow, grid); +} + +static void +calls_contacts_row_init (CallsContactsRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +calls_contacts_row_new (FolksIndividual *item) +{ + CallsContactsRow *self; + + g_return_val_if_fail (FOLKS_IS_INDIVIDUAL (item), NULL); + + self = g_object_new (CALLS_TYPE_CONTACTS_ROW, NULL); + self->item = g_object_ref (item); + + g_object_bind_property (item, "display-name", + self->title, "label", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (item, "display-name", + self->avatar, "text", + G_BINDING_SYNC_CREATE); + + g_signal_connect_object (item, + "notify::phone-numbers", + G_CALLBACK (phone_numbers_changed_cb), + self, G_CONNECT_SWAPPED); + + g_signal_connect_object (item, + "notify::avatar", + G_CALLBACK (avatar_changed_cb), + self, G_CONNECT_SWAPPED); + + avatar_changed_cb (self); + phone_numbers_changed_cb (self); + + return GTK_WIDGET (self); +} + + +FolksIndividual * +calls_contacts_row_get_item (CallsContactsRow *self) +{ + g_return_val_if_fail (CALLS_IS_CONTACTS_ROW (self), NULL); + + return self->item; +} diff --git a/src/calls-contacts-row.h b/src/calls-contacts-row.h new file mode 100644 index 0000000..11464c8 --- /dev/null +++ b/src/calls-contacts-row.h @@ -0,0 +1,27 @@ +/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* calls-contacts-row.h + * + * Copyright 2020 Purism SPC + * + * Author(s): + * Mohammed Sadiq + * Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define CALLS_TYPE_CONTACTS_ROW (calls_contacts_row_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsContactsRow, calls_contacts_row, CALLS, CONTACTS_ROW, GtkListBoxRow) + +GtkWidget *calls_contacts_row_new (FolksIndividual *item); +FolksIndividual *calls_contacts_row_get_item (CallsContactsRow *self); + +G_END_DECLS diff --git a/src/calls-main-window.c b/src/calls-main-window.c index 4a823a6..d2ef0e3 100644 --- a/src/calls-main-window.c +++ b/src/calls-main-window.c @@ -27,6 +27,7 @@ #include "calls-ussd.h" #include "calls-call-selector-item.h" #include "calls-new-call-box.h" +#include "calls-contacts-box.h" #include "calls-history-box.h" #include "calls-in-app-notification.h" #include "calls-manager.h" @@ -357,6 +358,15 @@ constructed (GObject *object) self); gtk_window_set_transient_for (GTK_WINDOW (self->ussd_dialog), GTK_WINDOW (self)); + // Add contacs box + widget = calls_contacts_box_new (); + gtk_stack_add_titled (self->main_stack, widget, + "contacts", _("Contacts")); + gtk_container_child_set (main_stack, widget, + "icon-name", "system-users-symbolic", + NULL); + gtk_widget_set_visible (widget, TRUE); + // Add new call box self->new_call = calls_new_call_box_new (); widget = GTK_WIDGET (self->new_call); @@ -365,7 +375,6 @@ constructed (GObject *object) gtk_container_child_set (main_stack, widget, "icon-name", "input-dialpad-symbolic", NULL); - // Add call records history = calls_history_box_new (self->record_store); widget = GTK_WIDGET (history); diff --git a/src/calls.gresources.xml b/src/calls.gresources.xml index 8ce1acd..e413bc0 100644 --- a/src/calls.gresources.xml +++ b/src/calls.gresources.xml @@ -12,6 +12,8 @@ new-call-header-bar.ui call-record-row.ui in-app-notification.ui + contacts-row.ui + contacts-box.ui style.css diff --git a/src/meson.build b/src/meson.build index 8020d4f..ac55a1a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -89,6 +89,8 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h', 'calls-in-app-notification.c', 'calls-in-app-notification.h', 'calls-manager.c', 'calls-manager.h', 'calls-notifier.c', 'calls-notifier.h', + 'calls-contacts-box.c', 'calls-contacts-box.h', + 'calls-contacts-row.c', 'calls-contacts-row.h', ]) calls_config_data = config_data diff --git a/src/style.css b/src/style.css index fc92944..153d778 100644 --- a/src/style.css +++ b/src/style.css @@ -22,26 +22,6 @@ keypad > grid > button, .dial-button, .delete-button { padding: 6px; } -/* HdyAvatar */ - -avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } - -avatar.color1 { background-image: linear-gradient(#ffbe6f, #ed6f00); color: #ffe5c5; } - -avatar.color2 { background-image: linear-gradient(#f8e45c, #e5a50a); color: #f9f4e1; } - -avatar.color3 { background-image: linear-gradient(#dc8add, #8a3ea3); color: #e7c2e8; } - -avatar.color4 { background-image: linear-gradient(#99c1f1, #337fdc); color: #cfe1f5; } - -avatar.color5 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } - -avatar.color6 { background-image: linear-gradient(#8de6ae, #29ae71); color: #cbf7d5; } - -avatar.color7 { background-image: linear-gradient(#f67365, #d91a23); color: #f6c8c4; } - -avatar.color8 { background-image: linear-gradient(#cdab8f, #865d3c); color: #e5d6ca; } - -avatar.contrasted { color: #fff; } - -/* End HdyAvatar */ +.no-background { + background: none; +} diff --git a/src/ui/contacts-box.ui b/src/ui/contacts-box.ui new file mode 100644 index 0000000..456f2b4 --- /dev/null +++ b/src/ui/contacts-box.ui @@ -0,0 +1,85 @@ + + + + + + + diff --git a/src/ui/contacts-row.ui b/src/ui/contacts-row.ui new file mode 100644 index 0000000..f4a9888 --- /dev/null +++ b/src/ui/contacts-row.ui @@ -0,0 +1,48 @@ + + + + diff --git a/src/ui/main-window.ui b/src/ui/main-window.ui index 485c9bb..c6393aa 100644 --- a/src/ui/main-window.ui +++ b/src/ui/main-window.ui @@ -47,18 +47,8 @@ True False + False True - - - True - True - - - contacts - Contacts - system-users-symbolic - -