Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-sms-window"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gtk/gtk.h>
10 : : #include <adwaita.h>
11 : : #include <valent.h>
12 : :
13 : : #include "valent-contact-row.h"
14 : : #include "valent-message.h"
15 : : #include "valent-sms-conversation.h"
16 : : #include "valent-sms-store.h"
17 : : #include "valent-sms-utils.h"
18 : : #include "valent-sms-window.h"
19 : : #include "valent-message-row.h"
20 : :
21 : :
22 : : struct _ValentSmsWindow
23 : : {
24 : : AdwApplicationWindow parent_instance;
25 : :
26 : : ValentContactStore *contact_store;
27 : : ValentSmsStore *message_store;
28 : :
29 : : /* template */
30 : : AdwNavigationSplitView *content_box;
31 : : AdwHeaderBar *content_header;
32 : : AdwHeaderBar *sidebar_header;
33 : :
34 : : AdwNavigationPage *content_page;
35 : : GtkBox *content_layout;
36 : : GtkListBox *conversation_list;
37 : : GtkStack *content;
38 : :
39 : : GtkWidget *message_search;
40 : : GtkWidget *message_search_entry;
41 : : GtkListBox *message_search_list;
42 : :
43 : : GtkWidget *contact_search;
44 : : GtkWidget *contact_search_entry;
45 : : GtkListBox *contact_search_list;
46 : : GtkWidget *placeholder_contact;
47 : : };
48 : :
49 [ + + + - ]: 18 : G_DEFINE_FINAL_TYPE (ValentSmsWindow, valent_sms_window, ADW_TYPE_APPLICATION_WINDOW)
50 : :
51 : : enum {
52 : : PROP_0,
53 : : PROP_CONTACT_STORE,
54 : : PROP_MESSAGE_STORE,
55 : : N_PROPERTIES
56 : : };
57 : :
58 : : static GParamSpec *properties[N_PROPERTIES] = { NULL, };
59 : :
60 : : enum {
61 : : SEND_MESSAGE,
62 : : N_SIGNALS
63 : : };
64 : :
65 : : static guint signals[N_SIGNALS] = { 0, };
66 : :
67 : :
68 : : /*
69 : : * Generic callback for querying a contact for a widget
70 : : */
71 : : static void
72 : 2 : phone_lookup_cb (ValentContactStore *store,
73 : : GAsyncResult *result,
74 : : ValentMessageRow *row)
75 : : {
76 : 4 : g_autoptr (GError) error = NULL;
77 [ - + ]: 2 : g_autoptr (EContact) contact = NULL;
78 : :
79 : 2 : contact = valent_sms_contact_from_phone_finish (store, result, &error);
80 : :
81 [ - + ]: 2 : if (contact == NULL)
82 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
83 : :
84 [ + - ]: 2 : valent_message_row_set_contact (row, contact);
85 : 2 : }
86 : :
87 : : static void
88 : 0 : search_contacts_cb (ValentContactStore *model,
89 : : GAsyncResult *result,
90 : : ValentSmsWindow *window)
91 : : {
92 : 0 : g_autoptr (GError) error = NULL;
93 [ # # ]: 0 : g_autoslist (GObject) contacts = NULL;
94 : :
95 : 0 : contacts = valent_contact_store_query_finish (model, result, &error);
96 : :
97 [ # # ]: 0 : for (const GSList *iter = contacts; iter; iter = iter->next)
98 : 0 : valent_list_add_contact (window->contact_search_list, iter->data);
99 : :
100 [ # # ]: 0 : if (error != NULL)
101 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
102 : 0 : }
103 : :
104 : : static void
105 : 0 : search_messages_cb (ValentSmsStore *store,
106 : : GAsyncResult *result,
107 : : ValentSmsWindow *window)
108 : : {
109 : 0 : g_autoptr (GError) error = NULL;
110 [ # # ]: 0 : g_autoptr (GPtrArray) messages = NULL;
111 : :
112 : 0 : messages = valent_sms_store_find_messages_finish (store, result, &error);
113 : :
114 [ # # ]: 0 : for (unsigned int i = 0; i < messages->len; i++)
115 : : {
116 : 0 : ValentMessage *message;
117 : 0 : GtkWidget *row;
118 : 0 : const char *address;
119 : :
120 : 0 : message = g_ptr_array_index (messages, i);
121 : :
122 : 0 : row = g_object_new (VALENT_TYPE_MESSAGE_ROW,
123 : : "message", message,
124 : : NULL);
125 : 0 : gtk_list_box_insert (window->message_search_list, row, -1);
126 : :
127 [ # # ]: 0 : if ((address = valent_message_get_sender (message)) == NULL)
128 : : {
129 : 0 : GVariant *metadata;
130 : 0 : g_autoptr (GVariant) addresses = NULL;
131 [ # # # # ]: 0 : g_autoptr (GVariant) address_dict = NULL;
132 : :
133 : 0 : metadata = valent_message_get_metadata (message);
134 : :
135 [ # # ]: 0 : if (!g_variant_lookup (metadata, "addresses", "@aa{sv}", &addresses))
136 : 0 : continue;
137 : :
138 [ # # ]: 0 : if (g_variant_n_children (addresses) == 0)
139 : 0 : continue;
140 : :
141 : 0 : address_dict = g_variant_get_child_value (addresses, 0);
142 : :
143 [ # # ]: 0 : if (!g_variant_lookup (address_dict, "address", "&s", &address))
144 [ # # ]: 0 : continue;
145 : : }
146 : :
147 : 0 : valent_sms_contact_from_phone (window->contact_store,
148 : : address,
149 : : NULL,
150 : : (GAsyncReadyCallback)phone_lookup_cb,
151 : : row);
152 : : }
153 : 0 : }
154 : :
155 : : /*
156 : : * Reset search pages in a timeout source
157 : : */
158 : : static gboolean
159 : 0 : reset_search_cb (gpointer data)
160 : : {
161 : 0 : ValentSmsWindow *self = data;
162 : :
163 [ # # ]: 0 : g_assert (VALENT_IS_SMS_WINDOW (self));
164 : :
165 : 0 : gtk_editable_set_text (GTK_EDITABLE (self->contact_search_entry), "");
166 : 0 : gtk_editable_set_text (GTK_EDITABLE (self->message_search_entry), "");
167 : :
168 : 0 : return G_SOURCE_REMOVE;
169 : : }
170 : :
171 : : /*
172 : : * Message Search
173 : : */
174 : : static void
175 : 0 : search_header_func (GtkListBoxRow *row,
176 : : GtkListBoxRow *before,
177 : : gpointer user_data)
178 : : {
179 [ # # ]: 0 : if (VALENT_IS_MESSAGE_ROW (row))
180 : : {
181 [ # # # # ]: 0 : if (before == NULL || !VALENT_IS_MESSAGE_ROW (before))
182 : : {
183 : 0 : GtkWidget *label;
184 : :
185 : 0 : label = g_object_new (GTK_TYPE_LABEL,
186 : : "label", _("Conversations"),
187 : : "halign", GTK_ALIGN_START,
188 : : "margin-end", 6,
189 : : "margin-start", 6,
190 : : "margin-top", 6,
191 : : NULL);
192 : 0 : gtk_widget_add_css_class (label, "dim-label");
193 : 0 : gtk_widget_add_css_class (label, "list-header-title");
194 : 0 : gtk_list_box_row_set_header (row, label);
195 : : }
196 : : }
197 : :
198 : 0 : valent_contact_row_header_func (row, before, user_data);
199 : 0 : }
200 : :
201 : : static void
202 : 0 : on_message_search_changed (GtkSearchEntry *entry,
203 : : ValentSmsWindow *window)
204 : : {
205 : 0 : GtkWidget *child;
206 : 0 : const char *query_str;
207 : 0 : EBookQuery *queries[2];
208 : 0 : g_autoptr (EBookQuery) query = NULL;
209 [ # # ]: 0 : g_autofree char *sexp = NULL;
210 : :
211 : 0 : query_str = gtk_editable_get_text (GTK_EDITABLE (entry));
212 : :
213 : : /* Clear previous results */
214 [ # # ]: 0 : while ((child = gtk_widget_get_first_child (GTK_WIDGET (window->message_search_list))))
215 : 0 : gtk_list_box_remove (window->message_search_list, child);
216 : :
217 : : /* NULL query */
218 [ # # ]: 0 : if (g_strcmp0 (query_str, "") == 0)
219 : 0 : return;
220 : :
221 : : /* Search messages */
222 : 0 : valent_sms_store_find_messages (window->message_store,
223 : : query_str,
224 : : NULL,
225 : : (GAsyncReadyCallback)search_messages_cb,
226 : : window);
227 : :
228 : : /* Search contacts */
229 : 0 : queries[0] = e_book_query_field_test (E_CONTACT_FULL_NAME,
230 : : E_BOOK_QUERY_CONTAINS,
231 : : query_str);
232 : 0 : queries[1] = e_book_query_field_test (E_CONTACT_TEL,
233 : : E_BOOK_QUERY_CONTAINS,
234 : : query_str);
235 : :
236 : 0 : query = e_book_query_or (G_N_ELEMENTS (queries), queries, TRUE);
237 : 0 : sexp = e_book_query_to_string (query);
238 : :
239 : 0 : valent_contact_store_query (window->contact_store,
240 : : sexp,
241 : : NULL,
242 : : (GAsyncReadyCallback)search_contacts_cb,
243 : : window);
244 : : }
245 : :
246 : : static void
247 : 0 : on_message_selected (GtkListBox *box,
248 : : GtkListBoxRow *row,
249 : : ValentSmsWindow *self)
250 : : {
251 : 0 : EContact *contact;
252 : :
253 [ # # ]: 0 : if (VALENT_IS_MESSAGE_ROW (row))
254 : : {
255 : 0 : ValentMessage *message;
256 : :
257 : 0 : message = valent_message_row_get_message (VALENT_MESSAGE_ROW (row));
258 : 0 : valent_sms_window_set_active_message (self, message);
259 : :
260 : : /* Reset the search after the transition */
261 : 0 : g_timeout_add_seconds (1, reset_search_cb, self);
262 : : }
263 [ # # ]: 0 : else if (VALENT_IS_CONTACT_ROW (row))
264 : : {
265 : 0 : contact = valent_contact_row_get_contact (VALENT_CONTACT_ROW (row));
266 : 0 : g_debug ("CONTACT ROW SELECTED %s",
267 : : (const char *)e_contact_get_const (contact, E_CONTACT_FULL_NAME));
268 : : }
269 : 0 : }
270 : :
271 : : /*
272 : : * Contact Search
273 : : */
274 : : static gboolean
275 : 1 : check_number (const char *query)
276 : : {
277 : 1 : static GRegex *is_number = NULL;
278 : :
279 [ + - ]: 1 : if (is_number == NULL)
280 : 1 : is_number = g_regex_new ("(?!0)[\\d]{3,}", G_REGEX_OPTIMIZE, 0, NULL);
281 : :
282 : 1 : return g_regex_match (is_number, query, 0, NULL);
283 : : }
284 : :
285 : : static void
286 : 0 : on_contact_selected (GtkListBox *box,
287 : : GtkListBoxRow *row,
288 : : ValentSmsWindow *self)
289 : : {
290 : 0 : const char *address;
291 : :
292 : 0 : address = valent_contact_row_get_contact_address (VALENT_CONTACT_ROW (row));
293 : 0 : g_debug ("NUMBER SELECTED: %s", address);
294 : 0 : }
295 : :
296 : : static void
297 : 1 : on_contact_search_changed (GtkSearchEntry *entry,
298 : : ValentSmsWindow *self)
299 : : {
300 : 1 : const char *query;
301 : :
302 : 1 : query = gtk_editable_get_text (GTK_EDITABLE (entry));
303 : :
304 : : /* If the entry contains a possible phone number... */
305 [ - + ]: 1 : if (check_number (query))
306 : : {
307 : 0 : EContact *contact;
308 : 0 : g_autofree char *name_label = NULL;
309 : :
310 : 0 : name_label = g_strdup_printf (_("Send to %s"), query);
311 : :
312 : : /* ...ensure we have a dynamic contact for it */
313 [ # # ]: 0 : if (self->placeholder_contact == NULL)
314 : : {
315 : : /* Create a dummy contact */
316 : 0 : contact = e_contact_new ();
317 : 0 : e_contact_set (contact, E_CONTACT_FULL_NAME, query);
318 : 0 : e_contact_set (contact, E_CONTACT_PHONE_OTHER, query);
319 : :
320 : : /* Create and add a new row */
321 : 0 : self->placeholder_contact = g_object_new (VALENT_TYPE_CONTACT_ROW,
322 : : "contact", contact,
323 : : "contact-name", name_label,
324 : : "contact-address", query,
325 : : NULL);
326 : :
327 : 0 : gtk_list_box_insert (self->contact_search_list,
328 : : self->placeholder_contact,
329 : : -1);
330 : : }
331 : :
332 : : /* ...or if we already do, then update it */
333 : : else
334 : : {
335 : 0 : g_object_get (self->placeholder_contact, "contact", &contact, NULL);
336 : :
337 : : /* Update contact */
338 : 0 : e_contact_set (contact, E_CONTACT_FULL_NAME, query);
339 : 0 : e_contact_set (contact, E_CONTACT_PHONE_OTHER, query);
340 : :
341 : : /* Update row */
342 : 0 : g_object_set (self->placeholder_contact,
343 : : "contact-name", name_label,
344 : : "contact-address", query,
345 : : NULL);
346 : : }
347 : :
348 : : /* Drop the extra ref */
349 [ # # ]: 0 : g_clear_pointer (&contact, g_object_unref);
350 : : }
351 : :
352 : : /* ...otherwise remove the dynamic row if created */
353 [ - + ]: 1 : else if (self->placeholder_contact != NULL)
354 : : {
355 : 0 : gtk_list_box_remove (self->contact_search_list,
356 : : self->placeholder_contact);
357 : 0 : self->placeholder_contact = NULL;
358 : : }
359 : :
360 : 1 : gtk_list_box_invalidate_filter (self->contact_search_list);
361 : 1 : gtk_list_box_invalidate_sort (self->contact_search_list);
362 : 1 : gtk_list_box_invalidate_headers (self->contact_search_list);
363 : 1 : }
364 : :
365 : : static gboolean
366 : 12 : contact_search_list_filter (ValentContactRow *row,
367 : : ValentSmsWindow *self)
368 : : {
369 : 12 : const char *query;
370 : 24 : g_autofree char *query_folded = NULL;
371 : 12 : g_autofree char *name = NULL;
372 : 12 : const char *address = NULL;
373 : :
374 : : /* Always show dynamic contact */
375 [ - + ]: 12 : if G_UNLIKELY (GTK_WIDGET (row) == self->placeholder_contact)
376 : : return TRUE;
377 : :
378 : 12 : query = gtk_editable_get_text (GTK_EDITABLE (self->contact_search_entry));
379 : :
380 [ + - ]: 12 : if (g_strcmp0 (query, "") == 0)
381 : : return TRUE;
382 : :
383 : 0 : query_folded = g_utf8_casefold (query, -1);
384 : :
385 : : /* Show contact if text is substring of name */
386 : 0 : name = g_utf8_casefold (valent_contact_row_get_contact_name (row), -1);
387 : :
388 [ # # ]: 0 : if (g_strrstr (name, query_folded) != NULL)
389 : : return TRUE;
390 : :
391 : : /* Show contact if text is substring of number */
392 : 0 : address = valent_contact_row_get_contact_address (row);
393 : :
394 [ # # ]: 0 : if (g_strrstr (address, query_folded))
395 : : return TRUE;
396 : :
397 : : return FALSE;
398 : : }
399 : :
400 : : static int
401 : 20 : contact_search_list_sort (GtkListBoxRow *row1,
402 : : GtkListBoxRow *row2,
403 : : ValentSmsWindow *self)
404 : : {
405 : 20 : const char *name1;
406 : 20 : const char *name2;
407 : :
408 [ + - ]: 20 : if G_UNLIKELY (GTK_WIDGET (row1) == self->placeholder_contact)
409 : : return -1;
410 : :
411 [ + - ]: 20 : if G_UNLIKELY (GTK_WIDGET (row2) == self->placeholder_contact)
412 : : return 1;
413 : :
414 : 20 : name1 = valent_contact_row_get_contact_name (VALENT_CONTACT_ROW (row1));
415 : 20 : name2 = valent_contact_row_get_contact_name (VALENT_CONTACT_ROW (row2));
416 : :
417 : 20 : return g_utf8_collate (name1, name2);
418 : : }
419 : :
420 : : static void
421 : 2 : refresh_contacts_cb (ValentContactStore *store,
422 : : GAsyncResult *result,
423 : : ValentSmsWindow *self)
424 : : {
425 : 2 : g_autoslist (GObject) contacts = NULL;
426 : 2 : g_autoptr (GError) error = NULL;
427 : :
428 : 2 : contacts = valent_contact_store_query_finish (store, result, &error);
429 : :
430 [ - + ]: 2 : if (error != NULL)
431 : : {
432 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
433 [ # # ]: 0 : return;
434 : : }
435 : :
436 [ + + ]: 5 : for (const GSList *iter = contacts; iter; iter = iter->next)
437 : 3 : valent_list_add_contact (self->contact_search_list, iter->data);
438 : : }
439 : :
440 : : static void
441 : 2 : valent_sms_window_refresh_contacts (ValentSmsWindow *self)
442 : : {
443 : 2 : GtkWidget *row;
444 : 0 : g_autoptr (EBookQuery) query = NULL;
445 [ + - ]: 2 : g_autofree char *sexp = NULL;
446 : :
447 : : /* Clear the list */
448 [ + + ]: 4 : while ((row = gtk_widget_get_first_child (GTK_WIDGET (self->contact_search_list))))
449 : 2 : gtk_list_box_remove (self->contact_search_list, row);
450 : :
451 [ - + ]: 2 : if (self->contact_store == NULL)
452 : 0 : return;
453 : :
454 : : /* Add the contacts */
455 : 2 : query = e_book_query_vcard_field_exists (EVC_TEL);
456 : 2 : sexp = e_book_query_to_string (query);
457 : :
458 : 2 : valent_contact_store_query (self->contact_store,
459 : : sexp,
460 : : NULL,
461 : : (GAsyncReadyCallback)refresh_contacts_cb,
462 : : self);
463 : : }
464 : :
465 : : /*
466 : : * Conversation List
467 : : */
468 : : static gboolean
469 : 0 : on_send_message (GObject *object,
470 : : ValentMessage *message,
471 : : ValentSmsWindow *window)
472 : : {
473 : 0 : gboolean sent;
474 : :
475 : 0 : g_signal_emit (G_OBJECT (window), signals [SEND_MESSAGE], 0, message, &sent);
476 : :
477 : 0 : return sent;
478 : : }
479 : :
480 : : /*
481 : : * Conversation List Callbacks
482 : : */
483 : : static GtkWidget *
484 : 2 : conversation_list_create (gpointer item,
485 : : gpointer user_data)
486 : : {
487 : 2 : ValentSmsWindow *window = VALENT_SMS_WINDOW (user_data);
488 : 2 : ValentMessage *message = VALENT_MESSAGE (item);
489 : 2 : GtkWidget *row;
490 : 2 : GVariant *metadata;
491 : 4 : g_autoptr (GVariant) addresses = NULL;
492 : :
493 : 2 : row = g_object_new (VALENT_TYPE_MESSAGE_ROW,
494 : : "message", message,
495 : : NULL);
496 : :
497 : : /* TODO: probably a failure of kdeconnect-android, but occasionally a message
498 : : * will have no addresses */
499 [ + - + - ]: 4 : if ((metadata = valent_message_get_metadata (message)) != NULL &&
500 [ + - ]: 4 : g_variant_lookup (metadata, "addresses", "@aa{sv}", &addresses) &&
501 : 2 : g_variant_n_children (addresses) > 0)
502 : : {
503 : 2 : g_autoptr (GVariant) participant = NULL;
504 : 2 : const char *address;
505 : :
506 : 2 : participant = g_variant_get_child_value (addresses, 0);
507 : :
508 [ + - ]: 2 : if (g_variant_lookup (participant, "address", "&s", &address))
509 : : {
510 : 2 : valent_sms_contact_from_phone (window->contact_store,
511 : : address,
512 : : NULL,
513 : : (GAsyncReadyCallback)phone_lookup_cb,
514 : : row);
515 : : }
516 : : }
517 : :
518 [ + - ]: 2 : return row;
519 : : }
520 : :
521 : : static void
522 : 2 : conversation_list_populate (ValentSmsWindow *window)
523 : : {
524 : 4 : g_autoptr (GListModel) threads = NULL;
525 : :
526 [ + - ]: 2 : g_assert (VALENT_IS_SMS_WINDOW (window));
527 : :
528 : 2 : threads = valent_sms_store_get_summary (window->message_store);
529 [ + - ]: 2 : gtk_list_box_bind_model (window->conversation_list,
530 : : threads,
531 : : conversation_list_create,
532 : : window,
533 : : NULL);
534 : 2 : }
535 : :
536 : : static GtkWidget *
537 : 1 : valent_sms_window_ensure_conversation (ValentSmsWindow *window,
538 : : int64_t thread_id)
539 : : {
540 : 1 : GtkWidget *conversation;
541 : 2 : g_autofree char *page_name = NULL;
542 : :
543 : 1 : page_name = g_strdup_printf ("%"G_GINT64_FORMAT, thread_id);
544 : 1 : conversation = gtk_stack_get_child_by_name (window->content, page_name);
545 : :
546 [ + - ]: 1 : if (conversation == NULL)
547 : : {
548 : 1 : conversation = g_object_new (VALENT_TYPE_SMS_CONVERSATION,
549 : : "contact-store", window->contact_store,
550 : : "message-store", window->message_store,
551 : : "thread-id", thread_id,
552 : : NULL);
553 : :
554 : 1 : g_object_bind_property (window, "contact-store",
555 : : conversation, "contact-store",
556 : : G_BINDING_DEFAULT);
557 : :
558 : 1 : g_signal_connect_object (conversation,
559 : : "send-message",
560 : : G_CALLBACK (on_send_message),
561 : : window, 0);
562 : :
563 : 1 : gtk_stack_add_named (window->content, conversation, page_name);
564 : : }
565 : :
566 : 1 : return conversation;
567 : : }
568 : :
569 : : static void
570 : 0 : on_conversation_activated (GtkListBox *box,
571 : : GtkListBoxRow *row,
572 : : ValentSmsWindow *self)
573 : : {
574 : 0 : ValentMessageRow *summary;
575 : 0 : int64_t thread_id;
576 : :
577 : : /* Deselect */
578 [ # # ]: 0 : if (row == NULL)
579 : : return;
580 : :
581 : 0 : summary = VALENT_MESSAGE_ROW (row);
582 : 0 : thread_id = valent_message_row_get_thread_id (summary);
583 : :
584 : 0 : valent_sms_window_set_active_thread (self, thread_id);
585 : 0 : adw_navigation_split_view_set_show_content (self->content_box, TRUE);
586 : : }
587 : :
588 : :
589 : : /*
590 : : * GActions
591 : : */
592 : : static void
593 : 0 : sms_new_action (GtkWidget *widget,
594 : : const char *action_name,
595 : : GVariant *parameter)
596 : : {
597 : 0 : ValentSmsWindow *self = VALENT_SMS_WINDOW (widget);
598 : :
599 [ # # ]: 0 : g_assert (VALENT_IS_SMS_WINDOW (self));
600 : :
601 : 0 : gtk_list_box_select_row (self->conversation_list, NULL);
602 : 0 : adw_navigation_page_set_title (self->content_page, _("New Conversation"));
603 : 0 : gtk_stack_set_visible_child_name (self->content, "contacts");
604 : 0 : gtk_widget_grab_focus (self->contact_search_entry);
605 : 0 : adw_navigation_split_view_set_show_content (self->content_box, TRUE);
606 : 0 : }
607 : :
608 : : static void
609 : 0 : sms_search_action (GtkWidget *widget,
610 : : const char *action_name,
611 : : GVariant *parameter)
612 : : {
613 : 0 : ValentSmsWindow *self = VALENT_SMS_WINDOW (widget);
614 : :
615 [ # # ]: 0 : g_assert (VALENT_IS_SMS_WINDOW (self));
616 : :
617 : 0 : adw_navigation_page_set_title (self->content_page, _("Search Messages"));
618 : 0 : gtk_stack_set_visible_child_name (self->content, "search");
619 : 0 : gtk_widget_grab_focus (self->message_search_entry);
620 : 0 : adw_navigation_split_view_set_show_content (self->content_box, TRUE);
621 : 0 : }
622 : :
623 : : /*
624 : : * GObject
625 : : */
626 : : static void
627 : 2 : valent_sms_window_constructed (GObject *object)
628 : : {
629 : 2 : ValentSmsWindow *self = VALENT_SMS_WINDOW (object);
630 : :
631 [ + - ]: 2 : g_assert (VALENT_IS_CONTACT_STORE (self->contact_store));
632 [ - + ]: 2 : g_assert (VALENT_IS_SMS_STORE (self->message_store));
633 : :
634 : : /* Prepare conversation summaries */
635 : 2 : conversation_list_populate (self);
636 : :
637 : 2 : G_OBJECT_CLASS (valent_sms_window_parent_class)->constructed (object);
638 : 2 : }
639 : :
640 : : static void
641 : 2 : valent_sms_window_dispose (GObject *object)
642 : : {
643 : 2 : GtkWidget *widget = GTK_WIDGET (object);
644 : :
645 : 2 : gtk_widget_dispose_template (widget, VALENT_TYPE_SMS_WINDOW);
646 : :
647 : 2 : G_OBJECT_CLASS (valent_sms_window_parent_class)->dispose (object);
648 : 2 : }
649 : :
650 : : static void
651 : 2 : valent_sms_window_finalize (GObject *object)
652 : : {
653 : 2 : ValentSmsWindow *self = VALENT_SMS_WINDOW (object);
654 : :
655 [ + - ]: 2 : g_clear_object (&self->contact_store);
656 [ + - ]: 2 : g_clear_object (&self->message_store);
657 : :
658 : 2 : G_OBJECT_CLASS (valent_sms_window_parent_class)->finalize (object);
659 : 2 : }
660 : :
661 : : static void
662 : 2 : valent_sms_window_get_property (GObject *object,
663 : : guint prop_id,
664 : : GValue *value,
665 : : GParamSpec *pspec)
666 : : {
667 : 2 : ValentSmsWindow *self = VALENT_SMS_WINDOW (object);
668 : :
669 [ + + - ]: 2 : switch (prop_id)
670 : : {
671 : 1 : case PROP_CONTACT_STORE:
672 : 1 : g_value_set_object (value, self->contact_store);
673 : 1 : break;
674 : :
675 : 1 : case PROP_MESSAGE_STORE:
676 : 1 : g_value_set_object (value, self->message_store);
677 : 1 : break;
678 : :
679 : 0 : default:
680 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
681 : : }
682 : 2 : }
683 : :
684 : : static void
685 : 4 : valent_sms_window_set_property (GObject *object,
686 : : guint prop_id,
687 : : const GValue *value,
688 : : GParamSpec *pspec)
689 : : {
690 : 4 : ValentSmsWindow *self = VALENT_SMS_WINDOW (object);
691 : :
692 [ + + - ]: 4 : switch (prop_id)
693 : : {
694 : 2 : case PROP_CONTACT_STORE:
695 : 2 : valent_sms_window_set_contact_store (self, g_value_get_object (value));
696 : 2 : break;
697 : :
698 : 2 : case PROP_MESSAGE_STORE:
699 : 2 : g_set_object (&self->message_store, g_value_get_object (value));
700 : 2 : break;
701 : :
702 : 0 : default:
703 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
704 : : }
705 : 4 : }
706 : :
707 : : static void
708 : 2 : valent_sms_window_class_init (ValentSmsWindowClass *klass)
709 : : {
710 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
711 : 2 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
712 : 4 : g_autoptr (GtkCssProvider) theme = NULL;
713 : :
714 : 2 : object_class->constructed = valent_sms_window_constructed;
715 : 2 : object_class->dispose = valent_sms_window_dispose;
716 : 2 : object_class->finalize = valent_sms_window_finalize;
717 : 2 : object_class->get_property = valent_sms_window_get_property;
718 : 2 : object_class->set_property = valent_sms_window_set_property;
719 : :
720 : 2 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/sms/valent-sms-window.ui");
721 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, content_box);
722 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, sidebar_header);
723 : :
724 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, content_page);
725 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, content_header);
726 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, content_layout);
727 : :
728 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, conversation_list);
729 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, content);
730 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_conversation_activated);
731 : :
732 : : /* Message Search */
733 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, message_search);
734 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, message_search_entry);
735 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, message_search_list);
736 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_message_search_changed);
737 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_message_selected);
738 : :
739 : : /* Contact Search */
740 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, contact_search);
741 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, contact_search_entry);
742 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsWindow, contact_search_list);
743 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_contact_search_changed);
744 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_contact_selected);
745 : :
746 : 2 : gtk_widget_class_install_action (widget_class, "sms.new", NULL, sms_new_action);
747 : 2 : gtk_widget_class_install_action (widget_class, "sms.search", NULL, sms_search_action);
748 : :
749 : : /**
750 : : * ValentSmsWindow:contact-store:
751 : : *
752 : : * The `ValentContactStore` providing contacts for the window.
753 : : */
754 : 4 : properties [PROP_CONTACT_STORE] =
755 : 2 : g_param_spec_object ("contact-store", NULL, NULL,
756 : : VALENT_TYPE_CONTACT_STORE,
757 : : (G_PARAM_READWRITE |
758 : : G_PARAM_CONSTRUCT_ONLY |
759 : : G_PARAM_EXPLICIT_NOTIFY |
760 : : G_PARAM_STATIC_STRINGS));
761 : :
762 : : /**
763 : : * ValentSmsWindow:message-store:
764 : : *
765 : : * The `ValentSmsStore` providing messages for the window.
766 : : */
767 : 4 : properties [PROP_MESSAGE_STORE] =
768 : 2 : g_param_spec_object ("message-store", NULL, NULL,
769 : : VALENT_TYPE_SMS_STORE,
770 : : (G_PARAM_READWRITE |
771 : : G_PARAM_CONSTRUCT_ONLY |
772 : : G_PARAM_EXPLICIT_NOTIFY |
773 : : G_PARAM_STATIC_STRINGS));
774 : :
775 : 2 : g_object_class_install_properties (object_class, N_PROPERTIES, properties);
776 : :
777 : : /**
778 : : * ValentSmsWindow::send-message:
779 : : * @window: a `ValentSmsWindow`
780 : : * @message: a `ValentMessage`
781 : : *
782 : : * The `ValentSmsWindow`::send-message signal is emitted when a child
783 : : * `ValentSmsConversation` emits `ValentSmsConversation`::send-message.
784 : : *
785 : : * The signal handler should return a boolean indicating success, although
786 : : * this only indicates the request was sent to the device.
787 : : */
788 : 4 : signals [SEND_MESSAGE] =
789 : 2 : g_signal_new ("send-message",
790 : : G_TYPE_FROM_CLASS (klass),
791 : : G_SIGNAL_RUN_LAST,
792 : : 0,
793 : : g_signal_accumulator_first_wins, NULL, NULL,
794 : : G_TYPE_BOOLEAN, 1, VALENT_TYPE_MESSAGE);
795 : :
796 : : /* Custom CSS */
797 : 2 : theme = gtk_css_provider_new ();
798 : 2 : gtk_css_provider_load_from_resource (theme, "/plugins/sms/sms.css");
799 [ + - ]: 2 : gtk_style_context_add_provider_for_display (gdk_display_get_default (),
800 : : GTK_STYLE_PROVIDER (theme),
801 : : GTK_STYLE_PROVIDER_PRIORITY_USER);
802 : 2 : }
803 : :
804 : : static void
805 : 2 : valent_sms_window_init (ValentSmsWindow *self)
806 : : {
807 : 2 : gtk_widget_init_template (GTK_WIDGET (self));
808 : :
809 : 2 : gtk_list_box_set_header_func (self->message_search_list,
810 : : search_header_func,
811 : : self, NULL);
812 : :
813 : : /* Contacts */
814 : 2 : gtk_list_box_set_filter_func (self->contact_search_list,
815 : : (GtkListBoxFilterFunc)contact_search_list_filter,
816 : : self,
817 : : NULL);
818 : :
819 : 2 : gtk_list_box_set_sort_func (self->contact_search_list,
820 : : (GtkListBoxSortFunc)contact_search_list_sort,
821 : : self,
822 : : NULL);
823 : :
824 : 2 : gtk_list_box_set_header_func (self->contact_search_list,
825 : : valent_contact_row_header_func,
826 : : self,
827 : : NULL);
828 : 2 : }
829 : :
830 : : /**
831 : : * valent_sms_window_get_contact_store:
832 : : * @window: a `ValentSmsWindow`
833 : : *
834 : : * Get the `ValentContactStore` providing contacts for @window.
835 : : *
836 : : * Returns: (transfer none) (nullable): a `ValentContactStore`
837 : : */
838 : : ValentContactStore *
839 : 1 : valent_sms_window_get_contact_store (ValentSmsWindow *window)
840 : : {
841 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_WINDOW (window), NULL);
842 : :
843 : 1 : return window->contact_store;
844 : : }
845 : :
846 : : /**
847 : : * valent_sms_window_set_contact_store:
848 : : * @window: a `ValentSmsWindow`
849 : : * @store: a `ValentContactStore`
850 : : *
851 : : * Set the `ValentContactStore` providing contacts for @window.
852 : : */
853 : : void
854 : 2 : valent_sms_window_set_contact_store (ValentSmsWindow *window,
855 : : ValentContactStore *store)
856 : : {
857 [ + - ]: 2 : g_return_if_fail (VALENT_IS_SMS_WINDOW (window));
858 [ + - - + ]: 2 : g_return_if_fail (store == NULL || VALENT_IS_CONTACT_STORE (store));
859 : :
860 [ + - ]: 2 : if (!g_set_object (&window->contact_store, store))
861 : : return;
862 : :
863 : 2 : valent_sms_window_refresh_contacts (window);
864 : 2 : g_object_notify_by_pspec (G_OBJECT (window), properties[PROP_CONTACT_STORE]);
865 : : }
866 : :
867 : : /**
868 : : * valent_sms_window_get_sms_store:
869 : : * @window: a `ValentSmsWindow`
870 : : *
871 : : * Get the `ValentSmsStore` providing messages for @window.
872 : : *
873 : : * Returns: (transfer none) (nullable): a `ValentSmsStore`
874 : : */
875 : : ValentSmsStore *
876 : 1 : valent_sms_window_get_message_store (ValentSmsWindow *window)
877 : : {
878 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_WINDOW (window), NULL);
879 : :
880 : 1 : return window->message_store;
881 : : }
882 : :
883 : : /**
884 : : * valent_sms_window_search_contacts:
885 : : * @window: a `ValentSmsWindow`
886 : : * @query: query string
887 : : *
888 : : * Switch the contact view and search for @query.
889 : : */
890 : : void
891 : 2 : valent_sms_window_search_contacts (ValentSmsWindow *window,
892 : : const char *query)
893 : : {
894 [ + - ]: 2 : g_return_if_fail (VALENT_IS_SMS_WINDOW (window));
895 [ - + ]: 2 : g_return_if_fail (query != NULL);
896 : :
897 : 2 : gtk_list_box_select_row (window->conversation_list, NULL);
898 : :
899 : 2 : adw_navigation_page_set_title (window->content_page, _("New Conversation"));
900 : 2 : gtk_stack_set_visible_child_name (window->content, "contacts");
901 : 2 : gtk_widget_grab_focus (window->contact_search_entry);
902 : 2 : adw_navigation_split_view_set_show_content (window->content_box, TRUE);
903 : :
904 : 2 : gtk_editable_set_text (GTK_EDITABLE (window->contact_search_entry), query);
905 : : }
906 : :
907 : : /**
908 : : * valent_sms_window_search_messages:
909 : : * @window: a `ValentSmsWindow`
910 : : * @query: query string
911 : : *
912 : : * Switch the search view and search for @query.
913 : : */
914 : : void
915 : 1 : valent_sms_window_search_messages (ValentSmsWindow *window,
916 : : const char *query)
917 : : {
918 [ + - ]: 1 : g_return_if_fail (VALENT_IS_SMS_WINDOW (window));
919 [ - + ]: 1 : g_return_if_fail (query != NULL);
920 : :
921 : 1 : adw_navigation_page_set_title (window->content_page, _("Search Messages"));
922 : 1 : gtk_stack_set_visible_child_name (window->content, "search");
923 : 1 : gtk_widget_grab_focus (window->message_search_entry);
924 : 1 : adw_navigation_split_view_set_show_content (window->content_box, TRUE);
925 : :
926 : 1 : gtk_editable_set_text (GTK_EDITABLE (window->message_search_entry), query);
927 : : }
928 : :
929 : : /**
930 : : * valent_sms_window_set_active_message:
931 : : * @window: a `ValentSmsWindow`
932 : : * @message: a `ValentMessage`
933 : : *
934 : : * Set the active conversation to the thread of @message scroll to @message.
935 : : */
936 : : void
937 : 0 : valent_sms_window_set_active_message (ValentSmsWindow *window,
938 : : ValentMessage *message)
939 : : {
940 : 0 : GtkWidget *widget;
941 : 0 : ValentSmsConversation *conversation;
942 : 0 : int64_t thread_id;
943 : :
944 [ # # ]: 0 : g_return_if_fail (VALENT_IS_SMS_WINDOW (window));
945 : :
946 : : /* Select the conversation */
947 : 0 : thread_id = valent_message_get_thread_id (message);
948 : 0 : valent_sms_window_set_active_thread (window, thread_id);
949 : :
950 : : /* Get the conversation */
951 : 0 : widget = valent_sms_window_ensure_conversation (window, thread_id);
952 : 0 : conversation = VALENT_SMS_CONVERSATION (widget);
953 : :
954 : 0 : valent_sms_conversation_scroll_to_message (conversation, message);
955 : : }
956 : :
957 : : /**
958 : : * valent_sms_window_set_active_thread:
959 : : * @window: a `ValentSmsWindow`
960 : : * @thread_id: a thread id
961 : : *
962 : : * Set the active conversation
963 : : */
964 : : void
965 : 1 : valent_sms_window_set_active_thread (ValentSmsWindow *window,
966 : : int64_t thread_id)
967 : : {
968 : 1 : GtkWidget *conversation;
969 : 1 : const char *title;
970 : :
971 [ + - ]: 1 : g_return_if_fail (VALENT_IS_SMS_WINDOW (window));
972 [ - + ]: 1 : g_return_if_fail (thread_id >= 0);
973 : :
974 : : /* Ensure a conversation widget exists */
975 : 1 : conversation = valent_sms_window_ensure_conversation (window, thread_id);
976 : :
977 : : /* Set the header title */
978 : 1 : title = valent_sms_conversation_get_title (VALENT_SMS_CONVERSATION (conversation));
979 : 1 : adw_navigation_page_set_title (window->content_page, title);
980 : :
981 : : /* Switch to conversation widget */
982 : 1 : gtk_stack_set_visible_child (window->content, conversation);
983 : : }
984 : :
|