From 3c22bc91545b6c524ab154d2f51a9cff0d89bc11 Mon Sep 17 00:00:00 2001 From: Bob Ham Date: Thu, 1 Aug 2019 14:25:53 +0100 Subject: [PATCH] Hook up Recent Calls list to database Closes use-cases#113 Closes use-cases#115 --- data/call-arrow-incoming-missed-symbolic.svg | 43 ++ data/call-arrow-incoming-symbolic.svg | 42 ++ data/call-arrow-outgoing-missed-symbolic.svg | 43 ++ data/call-arrow-outgoing-symbolic.svg | 42 ++ src/calls-application.c | 12 +- src/calls-call-record-row.c | 458 +++++++++++++++++++ src/calls-call-record-row.h | 46 ++ src/calls-history-box.c | 194 +++++++- src/calls-history-box.h | 7 +- src/calls-main-window.c | 63 ++- src/calls-main-window.h | 5 +- src/calls-new-call-box.c | 53 ++- src/calls-new-call-box.h | 4 +- src/calls-record-store.c | 165 ++++++- src/calls-record-store.h | 2 +- src/calls.gresources.xml | 5 + src/meson.build | 1 + src/ui/call-record-row.ui | 91 ++++ src/ui/history-box.ui | 28 +- src/ui/main-window.ui | 11 - src/util.c | 48 +- src/util.h | 31 +- 22 files changed, 1312 insertions(+), 82 deletions(-) create mode 100644 data/call-arrow-incoming-missed-symbolic.svg create mode 100644 data/call-arrow-incoming-symbolic.svg create mode 100644 data/call-arrow-outgoing-missed-symbolic.svg create mode 100644 data/call-arrow-outgoing-symbolic.svg create mode 100644 src/calls-call-record-row.c create mode 100644 src/calls-call-record-row.h create mode 100644 src/ui/call-record-row.ui diff --git a/data/call-arrow-incoming-missed-symbolic.svg b/data/call-arrow-incoming-missed-symbolic.svg new file mode 100644 index 0000000..1459c09 --- /dev/null +++ b/data/call-arrow-incoming-missed-symbolic.svg @@ -0,0 +1,43 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + diff --git a/data/call-arrow-incoming-symbolic.svg b/data/call-arrow-incoming-symbolic.svg new file mode 100644 index 0000000..ace7f8d --- /dev/null +++ b/data/call-arrow-incoming-symbolic.svg @@ -0,0 +1,42 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + diff --git a/data/call-arrow-outgoing-missed-symbolic.svg b/data/call-arrow-outgoing-missed-symbolic.svg new file mode 100644 index 0000000..f02be2f --- /dev/null +++ b/data/call-arrow-outgoing-missed-symbolic.svg @@ -0,0 +1,43 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + diff --git a/data/call-arrow-outgoing-symbolic.svg b/data/call-arrow-outgoing-symbolic.svg new file mode 100644 index 0000000..974ba36 --- /dev/null +++ b/data/call-arrow-outgoing-symbolic.svg @@ -0,0 +1,42 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + diff --git a/src/calls-application.c b/src/calls-application.c index 2ca5c26..8337a35 100644 --- a/src/calls-application.c +++ b/src/calls-application.c @@ -1,6 +1,6 @@ /* calls-application.c * - * Copyright (C) 2018 Purism SPC + * Copyright (C) 2018, 2019 Purism SPC * Copyright (C) 2018 Mohammed Sadiq * * This file is part of Calls. @@ -126,11 +126,16 @@ static const GActionEntry actions[] = static void startup (GApplication *application) { + GtkIconTheme *icon_theme; + G_APPLICATION_CLASS (calls_application_parent_class)->startup (application); g_set_prgname (APP_ID); g_set_application_name (_("Calls")); + icon_theme = gtk_icon_theme_get_default (); + gtk_icon_theme_add_resource_path (icon_theme, "/sm/puri/calls/"); + g_action_map_add_action_entries (G_ACTION_MAP (application), actions, G_N_ELEMENTS (actions), @@ -253,7 +258,9 @@ activate (GApplication *application) * But we assume that the application is closed by closing the * window. In that case, GTK+ frees the resources right. */ - window = GTK_WINDOW (calls_main_window_new (gtk_app, self->provider)); + window = GTK_WINDOW + (calls_main_window_new (gtk_app, self->provider, + G_LIST_MODEL (self->record_store))); calls_call_window_new (gtk_app, self->provider); } @@ -282,6 +289,7 @@ dispose (GObject *object) { CallsApplication *self = (CallsApplication *)object; + g_clear_object (&self->record_store); g_clear_object (&self->ringer); g_clear_object (&self->provider); diff --git a/src/calls-call-record-row.c b/src/calls-call-record-row.c new file mode 100644 index 0000000..e8f30d3 --- /dev/null +++ b/src/calls-call-record-row.c @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2018, 2019 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: Bob Ham + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "calls-call-record-row.h" +#include "util.h" + +#include +#include +#include + +#include +#include + + +struct _CallsCallRecordRow +{ + GtkOverlay parent_instance; + + GtkImage *avatar; + GtkImage *type; + GtkLabel *target; + GtkLabel *time; + + CallsCallRecord *record; + gulong answered_notify_handler_id; + gulong end_notify_handler_id; + guint date_change_timeout; + + CallsNewCallBox *new_call; +}; + +G_DEFINE_TYPE (CallsCallRecordRow, calls_call_record_row, GTK_TYPE_BOX); + + +enum { + PROP_0, + PROP_RECORD, + PROP_NEW_CALL, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +static void +redial_clicked_cb (CallsCallRecordRow *self) +{ + gchar *target; + + g_object_get (self->record, + "target", &target, + NULL); + g_assert (target != NULL); + + calls_new_call_box_dial (self->new_call, target); + g_free (target); +} + + +static void +nice_time (GDateTime *t, + gchar **nice, + gboolean *final) +{ + GDateTime *now = g_date_time_new_now_local (); + const gboolean today = + calls_date_time_is_same_day (now, t); + const gboolean yesterday = + (!today && calls_date_time_is_yesterday (now, t)); + + g_assert (nice != NULL); + g_assert (final != NULL); + + if (today || yesterday) + { + gchar *n = g_date_time_format (t, "%R"); + + if (yesterday) + { + gchar *s; + s = g_strdup_printf (_("%s\nyesterday"), n); + g_free (n); + n = s; + } + + *nice = n; + *final = FALSE; + } + else if (calls_date_time_is_same_year (now, t)) + { + *nice = g_date_time_format (t, "%b %-d"); + *final = FALSE; + } + else + { + *nice = g_date_time_format (t, "%Y"); + *final = TRUE; + } + + g_date_time_unref (now); +} + + +static void +update_time (CallsCallRecordRow *self, + GDateTime *end, + gboolean *final) +{ + gchar *nice; + nice_time (end, &nice, final); + gtk_label_set_text (self->time, nice); + g_free (nice); +} + + +static gboolean date_change_cb (CallsCallRecordRow *self); + + +static void +setup_date_change_timeout (CallsCallRecordRow *self) +{ + GDateTime *gnow, *gnextday, *gtomorrow; + struct timeval now, tomorrow, delta; + int err; + guint interval; + + // Get the time now + gnow = g_date_time_new_now_local (); + + // Get the next day + gnextday = g_date_time_add_days (gnow, 1); + g_date_time_unref (gnow); + + // Get the start of the next day + gtomorrow = + g_date_time_new (g_date_time_get_timezone (gnextday), + g_date_time_get_year (gnextday), + g_date_time_get_month (gnextday), + g_date_time_get_day_of_month (gnextday), + 0, + 0, + 0.0); + g_date_time_unref (gnextday); + + // Convert to a timeval + tomorrow.tv_sec = g_date_time_to_unix (gtomorrow); + tomorrow.tv_usec = 0; + g_date_time_unref (gtomorrow); + + // Get the precise time now + err = gettimeofday (&now, NULL); + if (err == -1) + { + g_warning ("Error getting time to set date change timeout: %s", + g_strerror (errno)); + return; + } + + // Find how long from now until the start of the next day + timersub (&tomorrow, &now, &delta); + + // Convert to milliseconds + interval = + (delta.tv_sec * 1000) + + + (delta.tv_usec / 1000); + + // Add the timeout + self->date_change_timeout = + g_timeout_add (interval, + (GSourceFunc)date_change_cb, + self); +} + + +static gboolean +date_change_cb (CallsCallRecordRow *self) +{ + GDateTime *end; + gboolean final; + + g_object_get (G_OBJECT (self->record), + "end", &end, + NULL); + g_assert (end != NULL); + + update_time (self, end, &final); + g_date_time_unref (end); + + if (final) + { + self->date_change_timeout = 0; + } + else + { + setup_date_change_timeout (self); + } + + return FALSE; +} + + +static void +update (CallsCallRecordRow *self, + gboolean inbound, + GDateTime *answered, + GDateTime *end) +{ + gboolean missed = FALSE; + gchar *type_icon_name; + + if (end) + { + gboolean time_final; + + update_time (self, end, &time_final); + + if (!time_final && !self->date_change_timeout) + { + setup_date_change_timeout (self); + } + + if (!answered) + { + missed = TRUE; + } + } + + type_icon_name = g_strdup_printf + ("call-arrow-%s%s-symbolic", + inbound ? "incoming" : "outgoing", + missed ? "-missed" : ""); + gtk_image_set_from_icon_name (self->type, type_icon_name, + GTK_ICON_SIZE_MENU); + + g_free (type_icon_name); +} + + +static void +notify_cb (CallsCallRecordRow *self, + GParamSpec *pspec, + CallsCallRecord *record) +{ + gboolean inbound; + GDateTime *answered; + GDateTime *end; + + g_object_get (G_OBJECT (self->record), + "inbound", &inbound, + "answered", &answered, + "end", &end, + NULL); + + update (self, inbound, answered, end); + + if (answered) + { + g_date_time_unref (answered); + calls_clear_signal (record, &self->answered_notify_handler_id); + } + if (end) + { + g_date_time_unref (end); + calls_clear_signal (record, &self->end_notify_handler_id); + } +} + + +static void +set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + CallsCallRecordRow *self = CALLS_CALL_RECORD_ROW (object); + + switch (property_id) { + case PROP_RECORD: + g_set_object (&self->record, + CALLS_CALL_RECORD (g_value_get_object (value))); + break; + + case PROP_NEW_CALL: + g_set_object (&self->new_call, + CALLS_NEW_CALL_BOX (g_value_get_object (value))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +constructed (GObject *object) +{ + GObjectClass *obj_class = g_type_class_peek (G_TYPE_OBJECT); + CallsCallRecordRow *self = CALLS_CALL_RECORD_ROW (object); + gchar *target; + gboolean inbound; + GDateTime *answered; + GDateTime *end; + + g_object_get (G_OBJECT (self->record), + "target", &target, + "inbound", &inbound, + "answered", &answered, + "end", &end, + NULL); + + gtk_label_set_text (self->target, target); + g_free (target); + + if (!end) + { + self->end_notify_handler_id = + g_signal_connect_swapped (self->record, + "notify::end", + G_CALLBACK (notify_cb), + self); + + if (!answered) + { + self->answered_notify_handler_id = + g_signal_connect_swapped (self->record, + "notify::answered", + G_CALLBACK (notify_cb), + self); + } + } + + update (self, inbound, answered, end); + calls_date_time_unref (answered); + calls_date_time_unref (end); + + obj_class->constructed (object); +} + + +static void +get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + CallsCallRecordRow *self = CALLS_CALL_RECORD_ROW (object); + + switch (property_id) { + case PROP_RECORD: + g_value_set_object (value, self->record); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +dispose (GObject *object) +{ + GObjectClass *obj_class = g_type_class_peek (G_TYPE_OBJECT); + CallsCallRecordRow *self = CALLS_CALL_RECORD_ROW (object); + + g_clear_object (&self->new_call); + + calls_clear_source (&self->date_change_timeout); + calls_clear_signal (self->record, &self->answered_notify_handler_id); + calls_clear_signal (self->record, &self->end_notify_handler_id); + g_clear_object (&self->record); + + obj_class->dispose (object); +} + + +static void +calls_call_record_row_class_init (CallsCallRecordRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = set_property; + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->dispose = dispose; + + props[PROP_RECORD] = + g_param_spec_object ("record", + _("Record"), + _("The call record for this row"), + CALLS_TYPE_CALL_RECORD, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_NEW_CALL] = + g_param_spec_object ("new-call", + _("New call"), + _("The UI box for making calls"), + CALLS_TYPE_NEW_CALL_BOX, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/call-record-row.ui"); + gtk_widget_class_bind_template_child (widget_class, CallsCallRecordRow, avatar); + gtk_widget_class_bind_template_child (widget_class, CallsCallRecordRow, type); + gtk_widget_class_bind_template_child (widget_class, CallsCallRecordRow, target); + gtk_widget_class_bind_template_child (widget_class, CallsCallRecordRow, time); + gtk_widget_class_bind_template_callback (widget_class, redial_clicked_cb); +} + + +static void +calls_call_record_row_init (CallsCallRecordRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +CallsCallRecordRow * +calls_call_record_row_new (CallsCallRecord *record, + CallsNewCallBox *new_call) +{ + return g_object_new (CALLS_TYPE_CALL_RECORD_ROW, + "record", record, + "new-call", new_call, + NULL); +} + + +CallsCallRecord * +calls_call_record_row_get_record (CallsCallRecordRow *self) +{ + return self->record; +} diff --git a/src/calls-call-record-row.h b/src/calls-call-record-row.h new file mode 100644 index 0000000..91c04cb --- /dev/null +++ b/src/calls-call-record-row.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018, 2019 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: Bob Ham + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#ifndef CALLS_CALL_RECORD_ROW_H__ +#define CALLS_CALL_RECORD_ROW_H__ + +#include "calls-call-record.h" +#include "calls-new-call-box.h" + +#include + +G_BEGIN_DECLS + +#define CALLS_TYPE_CALL_RECORD_ROW (calls_call_record_row_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsCallRecordRow, calls_call_record_row, + CALLS, CALL_RECORD_ROW, GtkBox); + +CallsCallRecordRow *calls_call_record_row_new (CallsCallRecord *record, + CallsNewCallBox *new_call); +CallsCallRecord * calls_call_record_row_get_record (CallsCallRecordRow *self); + +G_END_DECLS + +#endif /* CALLS_CALL_RECORD_ROW_H__ */ diff --git a/src/calls-history-box.c b/src/calls-history-box.c index 1b50577..5b53735 100644 --- a/src/calls-history-box.c +++ b/src/calls-history-box.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Purism SPC + * Copyright (C) 2018, 2019 Purism SPC * * This file is part of Calls. * @@ -23,9 +23,8 @@ */ #include "calls-history-box.h" -#include "calls-origin.h" -#include "calls-call-holder.h" -#include "calls-call-selector-item.h" +#include "calls-call-record.h" +#include "calls-call-record-row.h" #include "util.h" #include @@ -39,10 +38,179 @@ struct _CallsHistoryBox { GtkStack parent_instance; - GtkListStore *history_store; + GtkListBox *history; + + GListModel *model; + gulong model_changed_handler_id; + + CallsNewCallBox *new_call; }; -G_DEFINE_TYPE (CallsHistoryBox, calls_history_box, GTK_TYPE_STACK) +G_DEFINE_TYPE (CallsHistoryBox, calls_history_box, GTK_TYPE_STACK); + + +enum { + PROP_0, + PROP_MODEL, + PROP_NEW_CALL, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +static void +update (CallsHistoryBox *self) +{ + gchar *child_name; + + if (g_list_model_get_n_items (self->model) == 0) + { + child_name = "empty"; + } + else + { + child_name = "history"; + + /* Transition should only ever be from empty to non-empty */ + if (self->model_changed_handler_id != 0) + { + calls_clear_signal (self->model, + &self->model_changed_handler_id); + } + } + + gtk_stack_set_visible_child_name (GTK_STACK (self), child_name); +} + + +static void +header_cb (GtkListBoxRow *row, + GtkListBoxRow *before, + CallsHistoryBox *self) +{ + if (!before) + { + return; + } + + if (!gtk_list_box_row_get_header (row)) + { + GtkWidget *header = + gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + + gtk_list_box_row_set_header (row, header); + } +} + + +static GtkWidget * +create_row_cb (CallsCallRecord *record, + CallsHistoryBox *self) +{ + return GTK_WIDGET (calls_call_record_row_new (record, self->new_call)); +} + + +static void +set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + CallsHistoryBox *self = CALLS_HISTORY_BOX (object); + + switch (property_id) + { + case PROP_MODEL: + g_set_object (&self->model, + G_LIST_MODEL (g_value_get_object (value))); + break; + + case PROP_NEW_CALL: + g_set_object (&self->new_call, + CALLS_NEW_CALL_BOX (g_value_get_object (value))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +constructed (GObject *object) +{ + GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT); + CallsHistoryBox *self = CALLS_HISTORY_BOX (object); + + g_assert (self->model != NULL); + + self->model_changed_handler_id = + g_signal_connect_swapped + (self->model, "items-changed", G_CALLBACK (update), self); + g_assert (self->model_changed_handler_id != 0); + + gtk_list_box_set_header_func (self->history, + (GtkListBoxUpdateHeaderFunc)header_cb, + self, + NULL); + + gtk_list_box_bind_model (self->history, + self->model, + (GtkListBoxCreateWidgetFunc)create_row_cb, + self, + NULL); + + update (self); + + parent_class->constructed (object); +} + + +static void +dispose (GObject *object) +{ + GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT); + CallsHistoryBox *self = CALLS_HISTORY_BOX (object); + + g_clear_object (&self->new_call); + g_clear_object (&self->model); + + parent_class->dispose (object); +} + + +static void +calls_history_box_class_init (CallsHistoryBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = set_property; + object_class->constructed = constructed; + object_class->dispose = dispose; + + props[PROP_MODEL] = + g_param_spec_object ("model", + _("model"), + _("The data store containing call records"), + G_TYPE_LIST_MODEL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_NEW_CALL] = + g_param_spec_object ("new-call", + _("New call"), + _("The UI box for making calls"), + CALLS_TYPE_NEW_CALL_BOX, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/history-box.ui"); + gtk_widget_class_bind_template_child (widget_class, CallsHistoryBox, history); +} static void @@ -52,12 +220,12 @@ calls_history_box_init (CallsHistoryBox *self) } -static void -calls_history_box_class_init (CallsHistoryBoxClass *klass) +CallsHistoryBox * +calls_history_box_new (GListModel *model, + CallsNewCallBox *new_call) { - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); - - - gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/calls/ui/history-box.ui"); - gtk_widget_class_bind_template_child (widget_class, CallsHistoryBox, history_store); + return g_object_new (CALLS_TYPE_HISTORY_BOX, + "model", model, + "new-call", new_call, + NULL); } diff --git a/src/calls-history-box.h b/src/calls-history-box.h index 5037046..b01e2b5 100644 --- a/src/calls-history-box.h +++ b/src/calls-history-box.h @@ -25,6 +25,8 @@ #ifndef CALLS_HISTORY_BOX_H__ #define CALLS_HISTORY_BOX_H__ +#include "calls-new-call-box.h" + #include #define HANDY_USE_UNSTABLE_API @@ -34,7 +36,10 @@ G_BEGIN_DECLS #define CALLS_TYPE_HISTORY_BOX (calls_history_box_get_type ()) -G_DECLARE_FINAL_TYPE (CallsHistoryBox, calls_history_box, CALLS, HISTORY_BOX, GtkStack) +G_DECLARE_FINAL_TYPE (CallsHistoryBox, calls_history_box, CALLS, HISTORY_BOX, GtkStack); + +CallsHistoryBox * calls_history_box_new (GListModel *model, + CallsNewCallBox *new_call); G_END_DECLS diff --git a/src/calls-main-window.c b/src/calls-main-window.c index 2db8a8d..6121bc5 100644 --- a/src/calls-main-window.c +++ b/src/calls-main-window.c @@ -27,6 +27,7 @@ #include "calls-call-holder.h" #include "calls-call-selector-item.h" #include "calls-new-call-box.h" +#include "calls-history-box.h" #include "calls-enumerate.h" #include "config.h" #include "util.h" @@ -43,6 +44,7 @@ struct _CallsMainWindow GtkApplicationWindow parent_instance; CallsProvider *provider; + GListModel *record_store; GtkRevealer *info_revealer; guint info_timeout; @@ -55,6 +57,8 @@ struct _CallsMainWindow HdyViewSwitcher *narrow_switcher; HdyViewSwitcherBar *switcher_bar; GtkStack *main_stack; + + CallsNewCallBox *new_call; }; G_DEFINE_TYPE (CallsMainWindow, calls_main_window, GTK_TYPE_APPLICATION_WINDOW); @@ -62,6 +66,7 @@ G_DEFINE_TYPE (CallsMainWindow, calls_main_window, GTK_TYPE_APPLICATION_WINDOW); enum { PROP_0, PROP_PROVIDER, + PROP_RECORD_STORE, PROP_LAST_PROP, }; static GParamSpec *props[PROP_LAST_PROP]; @@ -194,7 +199,13 @@ set_property (GObject *object, switch (property_id) { case PROP_PROVIDER: - g_set_object (&self->provider, CALLS_PROVIDER (g_value_get_object (value))); + g_set_object (&self->provider, + CALLS_PROVIDER (g_value_get_object (value))); + break; + + case PROP_RECORD_STORE: + g_set_object (&self->record_store, + G_LIST_MODEL (g_value_get_object (value))); break; default: @@ -236,24 +247,39 @@ set_up_provider (CallsMainWindow *self) static void constructed (GObject *object) { - GObjectClass *parent_class = g_type_class_peek (GTK_TYPE_APPLICATION_WINDOW); + GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT); CallsMainWindow *self = CALLS_MAIN_WINDOW (object); GSimpleActionGroup *simple_action_group; - CallsNewCallBox *new_call_box; + GtkContainer *main_stack = GTK_CONTAINER (self->main_stack); + GtkWidget *widget; + CallsHistoryBox *history; set_up_provider (self); - /* Add new call box */ - new_call_box = calls_new_call_box_new (self->provider); - gtk_stack_add_titled (self->main_stack, GTK_WIDGET (new_call_box), + // Add new call box + self->new_call = calls_new_call_box_new (self->provider); + widget = GTK_WIDGET (self->new_call); + gtk_stack_add_titled (self->main_stack, widget, "dial-pad", _("Dial Pad")); - gtk_container_child_set (GTK_CONTAINER (self->main_stack), - GTK_WIDGET (new_call_box), + gtk_container_child_set (main_stack, widget, "icon-name", "input-dialpad-symbolic", NULL); - gtk_stack_set_visible_child_name (self->main_stack, "dial-pad"); - /* Add actions */ + // Add call records + history = calls_history_box_new (self->record_store, + self->new_call); + widget = GTK_WIDGET (history); + gtk_stack_add_titled (self->main_stack, widget, + "recent", _("Recent")); + gtk_container_child_set + (main_stack, widget, + "icon-name", "document-open-recent-symbolic", + "position", 0, + NULL); + gtk_widget_set_visible (widget, TRUE); + gtk_stack_set_visible_child_name (self->main_stack, "recent"); + + // Add actions simple_action_group = g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP (simple_action_group), window_entries, @@ -279,13 +305,15 @@ constructed (GObject *object) } + static void dispose (GObject *object) { - GObjectClass *parent_class = g_type_class_peek (GTK_TYPE_APPLICATION_WINDOW); + GObjectClass *parent_class = g_type_class_peek (G_TYPE_OBJECT); CallsMainWindow *self = CALLS_MAIN_WINDOW (object); stop_info_timeout (self); + g_clear_object (&self->record_store); g_clear_object (&self->provider); parent_class->dispose (object); @@ -327,6 +355,13 @@ calls_main_window_class_init (CallsMainWindowClass *klass) CALLS_TYPE_PROVIDER, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + props[PROP_RECORD_STORE] = + g_param_spec_object ("record-store", + _("Record store"), + _("The store of call records"), + G_TYPE_LIST_MODEL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); @@ -354,13 +389,17 @@ calls_main_window_init (CallsMainWindow *self) CallsMainWindow * -calls_main_window_new (GtkApplication *application, CallsProvider *provider) +calls_main_window_new (GtkApplication *application, + CallsProvider *provider, + GListModel *record_store) { g_return_val_if_fail (GTK_IS_APPLICATION (application), NULL); g_return_val_if_fail (CALLS_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (G_IS_LIST_MODEL (record_store), NULL); return g_object_new (CALLS_TYPE_MAIN_WINDOW, "application", application, "provider", provider, + "record-store", record_store, NULL); } diff --git a/src/calls-main-window.h b/src/calls-main-window.h index bb707b6..d40c06e 100644 --- a/src/calls-main-window.h +++ b/src/calls-main-window.h @@ -35,8 +35,9 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (CallsMainWindow, calls_main_window, CALLS, MAIN_WINDOW, GtkApplicationWindow); -CallsMainWindow *calls_main_window_new (GtkApplication *application, - CallsProvider *provider); +CallsMainWindow *calls_main_window_new (GtkApplication *application, + CallsProvider *provider, + GListModel *record_store); G_END_DECLS diff --git a/src/calls-new-call-box.c b/src/calls-new-call-box.c index 55923e1..b073bc6 100644 --- a/src/calls-new-call-box.c +++ b/src/calls-new-call-box.c @@ -79,30 +79,11 @@ dial_pad_deleted_cb (CallsNewCallBox *self, static void -dial_clicked_cb (CallsNewCallBox *self, - const gchar *unused, - GtkButton *button) +dial_clicked_cb (CallsNewCallBox *self) { - GtkTreeIter iter; - gboolean ok; - CallsOrigin *origin; - const gchar *number; - - ok = gtk_combo_box_get_active_iter (self->origin_box, &iter); - if (!ok) - { - g_debug ("Can't submit call with no origin"); - return; - } - - gtk_tree_model_get (GTK_TREE_MODEL (self->origin_store), &iter, - ORIGIN_STORE_COLUMN_ORIGIN, &origin, - -1); - g_assert (CALLS_IS_ORIGIN (origin)); - - number = gtk_entry_get_text (GTK_ENTRY (self->number_entry)); - - calls_origin_dial (origin, number); + calls_new_call_box_dial + (self, + gtk_entry_get_text (GTK_ENTRY (self->number_entry))); } @@ -311,3 +292,29 @@ calls_new_call_box_new (CallsProvider *provider) "provider", provider, NULL); } + +void +calls_new_call_box_dial (CallsNewCallBox *self, + const gchar *target) +{ + GtkTreeIter iter; + gboolean ok; + CallsOrigin *origin; + + g_return_if_fail (CALLS_IS_NEW_CALL_BOX (self)); + g_return_if_fail (target != NULL); + + ok = gtk_combo_box_get_active_iter (self->origin_box, &iter); + if (!ok) + { + g_debug ("Can't submit call with no origin"); + return; + } + + gtk_tree_model_get (GTK_TREE_MODEL (self->origin_store), &iter, + ORIGIN_STORE_COLUMN_ORIGIN, &origin, + -1); + g_assert (CALLS_IS_ORIGIN (origin)); + + calls_origin_dial (origin, target); +} diff --git a/src/calls-new-call-box.h b/src/calls-new-call-box.h index e848240..3ee483a 100644 --- a/src/calls-new-call-box.h +++ b/src/calls-new-call-box.h @@ -35,7 +35,9 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (CallsNewCallBox, calls_new_call_box, CALLS, NEW_CALL_BOX, GtkBox); -CallsNewCallBox * calls_new_call_box_new (CallsProvider *provider); +CallsNewCallBox * calls_new_call_box_new (CallsProvider *provider); +void calls_new_call_box_dial (CallsNewCallBox *self, + const gchar *target); G_END_DECLS diff --git a/src/calls-record-store.c b/src/calls-record-store.c index e413f29..7c90497 100644 --- a/src/calls-record-store.c +++ b/src/calls-record-store.c @@ -79,7 +79,7 @@ struct _CallsRecordStore GomRepository *repository; }; -G_DEFINE_TYPE (CallsRecordStore, calls_record_store, G_TYPE_OBJECT); +G_DEFINE_TYPE (CallsRecordStore, calls_record_store, G_TYPE_LIST_STORE); enum { @@ -90,6 +90,133 @@ enum { static GParamSpec *props[PROP_LAST_PROP]; +static void +load_calls_fetch_cb (GomResourceGroup *group, + GAsyncResult *res, + CallsRecordStore *self) +{ + gboolean ok; + GError *error = NULL; + guint count, i; + gpointer *records; + + ok = gom_resource_group_fetch_finish (group, + res, + &error); + if (error) + { + g_debug ("Error fetching call records: %s", + error->message); + g_error_free (error); + return; + } + g_assert (ok); + + count = gom_resource_group_get_count (group); + g_debug ("Fetched %u call records from database `%s'", + count, self->filename); + records = g_new (gpointer, count); + + for (i = 0; i < count; ++i) + { + GomResource *resource; + CallsCallRecord *record; + GDateTime *end = NULL; + + resource = gom_resource_group_get_index (group, i); + g_assert (resource != NULL); + g_assert (CALLS_IS_CALL_RECORD (resource)); + record = CALLS_CALL_RECORD (resource); + + records[i] = record; + + g_object_get (G_OBJECT (record), + "end", &end, + NULL); + if (end) + { + g_date_time_unref (end); + } + } + + g_list_store_splice (G_LIST_STORE (self), + 0, + 0, + records, + count); + + g_free (records); + g_object_unref (group); +} + + +static void +load_calls_find_cb (GomRepository *repository, + GAsyncResult *res, + CallsRecordStore *self) +{ + GomResourceGroup *group; + GError *error = NULL; + guint count; + + group = gom_repository_find_finish (repository, + res, + &error); + if (error) + { + g_debug ("Error finding call records in database `%s': %s", + self->filename, error->message); + g_error_free (error); + return; + } + g_assert (group != NULL); + + count = gom_resource_group_get_count (group); + if (count == 0) + { + g_debug ("No call records found in database `%s'", + self->filename); + return; + } + + g_debug ("Found %u call records in database `%s', fetching", + count, self->filename); + gom_resource_group_fetch_async + (group, + 0, + count, + (GAsyncReadyCallback)load_calls_fetch_cb, + self); +} + + +static void +load_calls (CallsRecordStore *self) +{ + GomFilter *filter; + GomSorting *sorting; + + filter = gom_filter_new_is_not_null + (CALLS_TYPE_CALL_RECORD, "start"); + + sorting = gom_sorting_new (CALLS_TYPE_CALL_RECORD, + "start", + GOM_SORTING_DESCENDING, + NULL); + + g_debug ("Finding records in call record database `%s'", + self->filename); + gom_repository_find_sorted_async (self->repository, + CALLS_TYPE_CALL_RECORD, + filter, + sorting, + (GAsyncReadyCallback)load_calls_find_cb, + self); + + g_object_unref (G_OBJECT (filter)); +} + + static void set_up_repo_migrate_cb (GomRepository *repo, GAsyncResult *res, @@ -120,6 +247,7 @@ set_up_repo_migrate_cb (GomRepository *repo, { g_debug ("Successfully migrated call record database `%s'", self->filename); + load_calls (self); } } @@ -256,12 +384,19 @@ open_repo (CallsRecordStore *self) } -static void -record_call_save_cb (GomResource *resource, - GAsyncResult *res, - CallsCall *call) +struct CallsRecordCallData { - GObject * const call_obj = G_OBJECT (call); + CallsRecordStore *self; + CallsCall *call; +}; + + +static void +record_call_save_cb (GomResource *resource, + GAsyncResult *res, + struct CallsRecordCallData *data) +{ + GObject * const call_obj = G_OBJECT (data->call); GError *error = NULL; gboolean ok; @@ -284,8 +419,15 @@ record_call_save_cb (GomResource *resource, else { g_debug ("Successfully saved new call record to database"); + g_list_store_insert (G_LIST_STORE (data->self), + 0, + CALLS_CALL_RECORD (resource)); g_object_set_data (call_obj, "calls-call-start", NULL); } + + g_object_unref (data->call); + g_object_unref (data->self); + g_free (data); } @@ -296,6 +438,7 @@ record_call (CallsRecordStore *self, GObject * const call_obj = G_OBJECT (call); GDateTime *start; CallsCallRecord *record; + struct CallsRecordCallData *data; g_assert (g_object_get_data (call_obj, "calls-call-record") == NULL); @@ -312,10 +455,15 @@ record_call (CallsRecordStore *self, g_object_set_data_full (call_obj, "calls-call-record", record, g_object_unref); + data = g_new (struct CallsRecordCallData, 1); + g_object_ref (self); + g_object_ref (call); + data->self = self; + data->call = call; gom_resource_save_async (GOM_RESOURCE (record), (GAsyncReadyCallback)record_call_save_cb, - call); + data); } @@ -550,6 +698,8 @@ dispose (GObject *object) g_clear_object (&self->provider); + g_list_store_remove_all (G_LIST_STORE (self)); + g_clear_object (&self->repository); close_adapter (self); @@ -604,6 +754,7 @@ CallsRecordStore * calls_record_store_new (CallsProvider *provider) { return g_object_new (CALLS_TYPE_RECORD_STORE, + "item-type", CALLS_TYPE_CALL_RECORD, "provider", provider, NULL); } diff --git a/src/calls-record-store.h b/src/calls-record-store.h index 791ca90..29de490 100644 --- a/src/calls-record-store.h +++ b/src/calls-record-store.h @@ -31,7 +31,7 @@ G_BEGIN_DECLS #define CALLS_TYPE_RECORD_STORE (calls_record_store_get_type ()) -G_DECLARE_FINAL_TYPE (CallsRecordStore, calls_record_store, CALLS, RECORD_STORE, GObject); +G_DECLARE_FINAL_TYPE (CallsRecordStore, calls_record_store, CALLS, RECORD_STORE, GListStore); CallsRecordStore *calls_record_store_new (CallsProvider *provider); diff --git a/src/calls.gresources.xml b/src/calls.gresources.xml index 34bc78e..f76110d 100644 --- a/src/calls.gresources.xml +++ b/src/calls.gresources.xml @@ -10,8 +10,13 @@ history-header-bar.ui new-call-box.ui new-call-header-bar.ui + call-record-row.ui new-call-symbolic.svg + call-arrow-incoming-symbolic.svg + call-arrow-incoming-missed-symbolic.svg + call-arrow-outgoing-symbolic.svg + call-arrow-outgoing-missed-symbolic.svg diff --git a/src/meson.build b/src/meson.build index 90c4545..e06b8f0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -54,6 +54,7 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h', 'util.c', 'util.h', 'calls-call-record.c', 'calls-call-record.h', 'calls-record-store.c', 'calls-record-store.h', + 'calls-call-record-row.c', 'calls-call-record-row.h', ]) calls_config_data = config_data diff --git a/src/ui/call-record-row.ui b/src/ui/call-record-row.ui new file mode 100644 index 0000000..a1e766b --- /dev/null +++ b/src/ui/call-record-row.ui @@ -0,0 +1,91 @@ + + + + + + diff --git a/src/ui/history-box.ui b/src/ui/history-box.ui index 078d30e..a7c868b 100644 --- a/src/ui/history-box.ui +++ b/src/ui/history-box.ui @@ -2,11 +2,9 @@ - - diff --git a/src/ui/main-window.ui b/src/ui/main-window.ui index 389f59e..49226a7 100644 --- a/src/ui/main-window.ui +++ b/src/ui/main-window.ui @@ -76,17 +76,6 @@ True False True - - - True - True - - - recent - Recent - document-open-recent-symbolic - - True diff --git a/src/util.c b/src/util.c index 065ae66..994fadf 100644 --- a/src/util.c +++ b/src/util.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Purism SPC + * Copyright (C) 2018, 2019 Purism SPC * * This file is part of Calls. * @@ -85,3 +85,49 @@ calls_entry_append (GtkEntry *entry, gtk_entry_buffer_insert_text (buf, len, str, 1); } + + +gboolean +calls_date_time_is_same_day (GDateTime *a, + GDateTime *b) +{ +#define eq(member) \ + (g_date_time_get_##member (a) == \ + g_date_time_get_##member (b)) + + return + eq (year) + && + eq (month) + && + eq (day_of_month); + +#undef eq +} + + +gboolean +calls_date_time_is_yesterday (GDateTime *now, + GDateTime *t) +{ + GDateTime *yesterday; + gboolean same_day; + + yesterday = g_date_time_add_days (now, -1); + + same_day = calls_date_time_is_same_day (yesterday, t); + + g_date_time_unref (yesterday); + + return same_day; +} + + +gboolean +calls_date_time_is_same_year (GDateTime *a, + GDateTime *b) +{ + return + g_date_time_get_year (a) == + g_date_time_get_year (b); +} diff --git a/src/util.h b/src/util.h index f915c2d..544b81c 100644 --- a/src/util.h +++ b/src/util.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Purism SPC + * Copyright (C) 2018, 2019 Purism SPC * * This file is part of Calls. * @@ -76,6 +76,27 @@ G_BEGIN_DECLS ptr = new_value; +#define calls_clear_source(source_id_ptr) \ + if (*source_id_ptr != 0) \ + { \ + g_source_remove (*source_id_ptr); \ + *source_id_ptr = 0; \ + } + +#define calls_clear_signal(object,handler_id_ptr) \ + if (*handler_id_ptr != 0) \ + { \ + g_signal_handler_disconnect (object, *handler_id_ptr); \ + *handler_id_ptr = 0; \ + } + +#define calls_date_time_unref(date_time) \ + if (date_time) \ + { \ + g_date_time_unref (date_time); \ + } + + /** Find a particular pointer value in a GtkListStore */ gboolean calls_list_store_find (GtkListStore *store, @@ -88,6 +109,14 @@ void calls_entry_append (GtkEntry *entry, gchar character); + +gboolean calls_date_time_is_same_day (GDateTime *a, + GDateTime *b); +gboolean calls_date_time_is_yesterday (GDateTime *now, + GDateTime *t); +gboolean calls_date_time_is_same_year (GDateTime *a, + GDateTime *b); + G_END_DECLS #endif /* CALLS__UTIL_H__ */