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-conversation"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gtk/gtk.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-date-label.h"
13 : : #include "valent-message.h"
14 : : #include "valent-message-thread.h"
15 : : #include "valent-sms-conversation.h"
16 : : #include "valent-sms-conversation-row.h"
17 : : #include "valent-sms-store.h"
18 : : #include "valent-sms-utils.h"
19 : :
20 : :
21 : : struct _ValentSmsConversation
22 : : {
23 : : GtkWidget parent_instance;
24 : :
25 : : /* template */
26 : : GtkWidget *message_view;
27 : : GtkListBox *message_list;
28 : : GtkWidget *message_entry;
29 : : GtkListBoxRow *pending;
30 : :
31 : : /* Population */
32 : : guint populate_id;
33 : : guint update_id;
34 : : double offset;
35 : : GtkAdjustment *vadjustment;
36 : :
37 : : /* Thread Resources */
38 : : int64_t loaded_id;
39 : : int64_t thread_id;
40 : : ValentSmsStore *message_store;
41 : : GListModel *thread;
42 : : unsigned int position_upper;
43 : : unsigned int position_lower;
44 : : ValentContactStore *contact_store;
45 : : GHashTable *participants;
46 : :
47 : : char *title;
48 : : char *subtitle;
49 : : };
50 : :
51 : : static void valent_sms_conversation_send_message (ValentSmsConversation *self);
52 : :
53 [ + + + - ]: 19 : G_DEFINE_FINAL_TYPE (ValentSmsConversation, valent_sms_conversation, GTK_TYPE_WIDGET)
54 : :
55 : : enum {
56 : : PROP_0,
57 : : PROP_CONTACT_STORE,
58 : : PROP_MESSAGE_STORE,
59 : : PROP_THREAD_ID,
60 : : N_PROPERTIES
61 : : };
62 : :
63 : : static GParamSpec *properties[N_PROPERTIES] = { NULL, };
64 : :
65 : : enum {
66 : : SEND_MESSAGE,
67 : : N_SIGNALS
68 : : };
69 : :
70 : : static guint signals[N_SIGNALS] = { 0, };
71 : :
72 : :
73 : : /* Callbacks */
74 : : static void
75 : 1 : phone_lookup_cb (ValentContactStore *store,
76 : : GAsyncResult *result,
77 : : GtkWidget *widget)
78 : : {
79 : 1 : g_autoptr (ValentSmsConversationRow) row = VALENT_SMS_CONVERSATION_ROW (widget);
80 [ - - + - ]: 1 : g_autoptr (EContact) contact = NULL;
81 [ - - ]: 1 : g_autoptr (GError) error = NULL;
82 : 1 : GtkWidget *conversation;
83 : :
84 : 1 : contact = valent_sms_contact_from_phone_finish (store, result, &error);
85 : :
86 [ - + ]: 1 : if (contact == NULL)
87 : : {
88 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
89 [ # # ]: 0 : return;
90 : : }
91 : :
92 : 1 : conversation = gtk_widget_get_ancestor (widget, VALENT_TYPE_SMS_CONVERSATION);
93 : :
94 [ + - ]: 1 : if (conversation != NULL)
95 : : {
96 : 1 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (conversation);
97 : 1 : ValentMessage *message;
98 : 1 : const char *sender;
99 : :
100 : 1 : message = valent_sms_conversation_row_get_message (row);
101 : 1 : sender = valent_message_get_sender (message);
102 : :
103 [ - + ]: 1 : g_hash_table_insert (self->participants,
104 : 1 : g_strdup (sender),
105 : : g_object_ref (contact));
106 : :
107 : 1 : valent_sms_conversation_row_set_contact (row, contact);
108 : : }
109 : : }
110 : :
111 : : static void
112 : 0 : valent_sms_conversation_scroll_to_row (ValentSmsConversation *self,
113 : : GtkWidget *widget)
114 : : {
115 : 0 : GtkScrolledWindow *scrolled = GTK_SCROLLED_WINDOW (self->message_view);
116 : 0 : GtkWidget *viewport;
117 : 0 : double upper, page_size;
118 : 0 : double x, y;
119 : :
120 : : /* Get the scrolled window state */
121 : 0 : upper = gtk_adjustment_get_upper (self->vadjustment);
122 : 0 : page_size = gtk_adjustment_get_page_size (self->vadjustment);
123 : :
124 : : /* Get the widget's position in the window */
125 : 0 : viewport = gtk_scrolled_window_get_child (scrolled);
126 : 0 : gtk_widget_translate_coordinates (widget, viewport, 0, 0, &x, &y);
127 : :
128 : : /* Scroll to the position */
129 [ # # ]: 0 : gtk_adjustment_set_value (self->vadjustment, CLAMP (y, page_size, upper));
130 : 0 : }
131 : :
132 : : static void
133 : 12 : message_list_header_func (GtkListBoxRow *row,
134 : : GtkListBoxRow *before,
135 : : gpointer user_data)
136 : : {
137 : 12 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (user_data);
138 : 12 : ValentSmsConversationRow *c_row = VALENT_SMS_CONVERSATION_ROW (row);
139 : 12 : ValentSmsConversationRow *b_row = VALENT_SMS_CONVERSATION_ROW (before);
140 : 12 : int64_t row_date, before_date;
141 : 12 : gboolean row_incoming, before_incoming;
142 : :
143 [ + - + - : 12 : g_assert (GTK_IS_LIST_BOX_ROW (row));
+ - - + ]
144 [ + + + - : 12 : g_assert (before == NULL || GTK_IS_LIST_BOX_ROW (before));
+ - - + ]
145 : :
146 : : /* TODO Skip pending */
147 [ + - ]: 12 : if G_UNLIKELY (row == self->pending)
148 : : return;
149 : :
150 : 12 : row_incoming = valent_sms_conversation_row_is_incoming (c_row);
151 : :
152 : : /* If this is the first row and it's incoming, show the avatar */
153 [ + + ]: 12 : if (before == NULL)
154 : : {
155 [ - + ]: 8 : if (row_incoming)
156 : 0 : valent_sms_conversation_row_show_avatar (c_row, TRUE);
157 : 8 : return;
158 : : }
159 : :
160 : : /* Date header */
161 : 4 : before_incoming = valent_sms_conversation_row_is_incoming (b_row);
162 : 4 : before_date = valent_sms_conversation_row_get_date (b_row);
163 : 4 : row_date = valent_sms_conversation_row_get_date (c_row);
164 : :
165 : : /* If it's been more than an hour between messages, show a date label */
166 [ - + ]: 4 : if (row_date - before_date > G_TIME_SPAN_HOUR / 1000)
167 : : {
168 : : /* Show a human-readable time span label */
169 [ # # ]: 0 : if G_UNLIKELY (gtk_list_box_row_get_header (row) == NULL)
170 : : {
171 : 0 : GtkWidget *header;
172 : :
173 : 0 : header = valent_date_label_new (row_date);
174 : 0 : gtk_widget_add_css_class (header, "dim-label");
175 : 0 : gtk_list_box_row_set_header (row, header);
176 : :
177 : : /* If the row's message is incoming, show the avatar also */
178 [ # # ]: 0 : if (row_incoming)
179 : 0 : valent_sms_conversation_row_show_avatar (c_row, row_incoming);
180 : : }
181 : : }
182 [ - + ]: 4 : else if (row_incoming)
183 : : {
184 : 0 : valent_sms_conversation_row_show_avatar (c_row, TRUE);
185 : :
186 : : /* If the previous row was incoming, hide its avatar */
187 [ # # ]: 0 : if (before_incoming)
188 : 0 : valent_sms_conversation_row_show_avatar (c_row, FALSE);
189 : : }
190 : : }
191 : :
192 : : /**
193 : : * valent_sms_conversation_insert_message:
194 : : * @conversation: a `ValentSmsConversation`
195 : : * @message: a `ValentMessage`
196 : : * @position: position to insert the widget
197 : : *
198 : : * Create a new message row for @message and insert it into the message list at
199 : : * @position.
200 : : *
201 : : * Returns: (transfer none): a `GtkWidget`
202 : : */
203 : : static GtkWidget *
204 : 4 : valent_sms_conversation_insert_message (ValentSmsConversation *self,
205 : : ValentMessage *message,
206 : : int position)
207 : : {
208 : 4 : ValentSmsConversationRow *row;
209 : 4 : const char *sender = NULL;
210 : 4 : EContact *contact = NULL;
211 : :
212 [ + - ]: 4 : g_assert (VALENT_IS_SMS_CONVERSATION (self));
213 [ - + ]: 4 : g_assert (VALENT_IS_MESSAGE (message));
214 : :
215 : : /* Create the row */
216 : 4 : row = g_object_new (VALENT_TYPE_SMS_CONVERSATION_ROW,
217 : : "message", message,
218 : : "activatable", FALSE,
219 : : "selectable", FALSE,
220 : : NULL);
221 : :
222 : : /* If the message has a sender, try to lookup the contact */
223 [ + + ]: 4 : if ((sender = valent_message_get_sender (message)) != NULL)
224 : : {
225 : 2 : GHashTableIter iter;
226 : 2 : const char *address = NULL;
227 : :
228 : 2 : g_hash_table_iter_init (&iter, self->participants);
229 : :
230 [ - + ]: 2 : while (g_hash_table_iter_next (&iter, (void **)&address, (void **)&contact))
231 : : {
232 [ # # ]: 0 : if (valent_phone_number_equal (sender, address))
233 : : {
234 : 0 : valent_sms_conversation_row_set_contact (row, contact);
235 : 0 : break;
236 : : }
237 : :
238 : 0 : contact = NULL;
239 : : }
240 : :
241 [ + - ]: 2 : if (contact == NULL)
242 : : {
243 : 2 : valent_sms_contact_from_phone (self->contact_store,
244 : : sender,
245 : : NULL,
246 : : (GAsyncReadyCallback)phone_lookup_cb,
247 : : g_object_ref_sink (row));
248 : : }
249 : : }
250 : :
251 : : /* Insert the row into the message list */
252 : 4 : gtk_list_box_insert (self->message_list, GTK_WIDGET (row), position);
253 : :
254 : 4 : return GTK_WIDGET (row);
255 : : }
256 : :
257 : : #if 0
258 : : /**
259 : : * valent_conversation_remove_message:
260 : : * @conversation: a `ValentSmsConversation`
261 : : * @message: a `ValentMessage`
262 : : *
263 : : * Remove a message from the conversation.
264 : : */
265 : : static void
266 : : valent_sms_conversation_remove_message (ValentSmsConversation *conversation,
267 : : int64_t message_id)
268 : : {
269 : : GtkWidget *child;
270 : :
271 : : g_assert (VALENT_IS_SMS_CONVERSATION (conversation));
272 : : g_assert (message_id > 0);
273 : :
274 : : for (child = gtk_widget_get_first_child (GTK_WIDGET (conversation->message_list));
275 : : child != NULL;
276 : : child = gtk_widget_get_next_sibling (child))
277 : : {
278 : : ValentSmsConversationRow *row = VALENT_SMS_CONVERSATION_ROW (child);
279 : :
280 : : if (valent_sms_conversation_row_get_id (row) == message_id)
281 : : {
282 : : gtk_list_box_remove (conversation->message_list, child);
283 : : break;
284 : : }
285 : : }
286 : : }
287 : : #endif
288 : :
289 : : /*
290 : : * Message Entry Callbacks
291 : : */
292 : : static void
293 : 0 : on_entry_activated (GtkEntry *entry,
294 : : ValentSmsConversation *conversation)
295 : : {
296 : 0 : valent_sms_conversation_send_message (conversation);
297 : 0 : }
298 : :
299 : : static void
300 : 0 : on_entry_icon_release (GtkEntry *entry,
301 : : GtkEntryIconPosition icon_pos,
302 : : ValentSmsConversation *conversation)
303 : : {
304 : 0 : valent_sms_conversation_send_message (conversation);
305 : 0 : }
306 : :
307 : : static void
308 : 0 : on_entry_changed (GtkEntry *entry,
309 : : ValentSmsConversation *conversation)
310 : : {
311 : 0 : const char *text;
312 : :
313 : 0 : text = gtk_editable_get_text (GTK_EDITABLE (entry));
314 : :
315 : 0 : gtk_entry_set_icon_sensitive (entry, GTK_ENTRY_ICON_SECONDARY, *text != '\0');
316 : 0 : }
317 : :
318 : : /*
319 : : * Auto-scroll
320 : : */
321 : : static inline ValentMessage *
322 : 6 : valent_sms_conversation_pop_tail (ValentSmsConversation *self)
323 : : {
324 [ + - ]: 6 : if G_UNLIKELY (self->thread == NULL)
325 : : return NULL;
326 : :
327 [ + + ]: 6 : if (self->position_lower == 0)
328 : : return NULL;
329 : :
330 : 4 : self->position_lower -= 1;
331 : :
332 : 4 : return g_list_model_get_item (self->thread, self->position_lower);
333 : : }
334 : :
335 : : #if 0
336 : : static inline ValentMessage *
337 : : valent_sms_conversation_pop_head (ValentSmsConversation *self)
338 : : {
339 : : if G_UNLIKELY (self->thread == NULL)
340 : : return NULL;
341 : :
342 : : if (self->position_upper == g_list_model_get_n_items (self->thread) - 1)
343 : : return NULL;
344 : :
345 : : self->position_upper += 1;
346 : :
347 : : return g_list_model_get_item (self->thread, self->position_upper);
348 : : }
349 : : #endif
350 : :
351 : : static void
352 : 2 : valent_sms_conversation_populate_reverse (ValentSmsConversation *self)
353 : : {
354 : 2 : unsigned int count = 10;
355 : 2 : unsigned int n_items;
356 : :
357 [ + - ]: 2 : if G_UNLIKELY (self->thread == NULL)
358 : : return;
359 : :
360 [ + - ]: 2 : if ((n_items = g_list_model_get_n_items (self->thread)) == 0)
361 : : return;
362 : :
363 [ + - ]: 2 : if (self->position_upper == self->position_lower)
364 : : {
365 : 2 : self->position_lower = n_items;
366 : 2 : self->position_upper = n_items - 1;
367 : : }
368 : :
369 [ + - ]: 6 : for (unsigned int i = 0; i < count; i++)
370 : : {
371 : 4 : g_autoptr (ValentMessage) message = NULL;
372 : :
373 [ + + ]: 6 : if ((message = valent_sms_conversation_pop_tail (self)) == NULL)
374 : : break;
375 : :
376 : 4 : valent_sms_conversation_insert_message (self, message, 0);
377 : : }
378 : :
379 : 2 : gtk_list_box_invalidate_headers (self->message_list);
380 : : }
381 : :
382 : : static gboolean
383 : 2 : valent_sms_conversation_populate (gpointer data)
384 : : {
385 : 2 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (data);
386 : 2 : double upper, value;
387 : :
388 : 2 : upper = gtk_adjustment_get_upper (self->vadjustment);
389 : 2 : value = gtk_adjustment_get_value (self->vadjustment);
390 : :
391 : 2 : self->offset = upper - value;
392 : 2 : valent_sms_conversation_populate_reverse (self);
393 : 2 : self->populate_id = 0;
394 : :
395 : 2 : return G_SOURCE_REMOVE;
396 : : }
397 : :
398 : : static inline void
399 : 2 : valent_sms_conversation_queue_populate (ValentSmsConversation *self)
400 : : {
401 [ + - ]: 2 : if (self->populate_id > 0)
402 : : return;
403 : :
404 : 2 : self->populate_id = g_idle_add_full (G_PRIORITY_LOW,
405 : : valent_sms_conversation_populate,
406 : : g_object_ref (self),
407 : : g_object_unref);
408 : : }
409 : :
410 : : static gboolean
411 : 1 : valent_sms_conversation_update (gpointer data)
412 : : {
413 : 1 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (data);
414 : 1 : double value;
415 : :
416 [ - + ]: 1 : if (self->offset > 0)
417 : : {
418 : 0 : value = gtk_adjustment_get_upper (self->vadjustment) - self->offset;
419 : 0 : self->offset = 0;
420 : 0 : gtk_adjustment_set_value (self->vadjustment, value);
421 : : }
422 : :
423 : 1 : self->update_id = 0;
424 : :
425 : 1 : return G_SOURCE_REMOVE;
426 : : }
427 : :
428 : : static inline void
429 : 1 : valent_sms_conversation_queue_update (ValentSmsConversation *self)
430 : : {
431 [ + - ]: 1 : if (self->update_id > 0)
432 : : return;
433 : :
434 : 1 : self->update_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
435 : : valent_sms_conversation_update,
436 : : g_object_ref (self),
437 : : g_object_unref);
438 : : }
439 : :
440 : : static void
441 : 0 : on_edge_overshot (GtkScrolledWindow *scrolled_window,
442 : : GtkPositionType pos,
443 : : ValentSmsConversation *self)
444 : : {
445 [ # # ]: 0 : if (pos == GTK_POS_TOP)
446 : 0 : valent_sms_conversation_queue_populate (self);
447 : :
448 [ # # ]: 0 : if (pos == GTK_POS_BOTTOM)
449 : 0 : self->offset = 0;
450 : 0 : }
451 : :
452 : : static void
453 : 1 : on_scroll_notify_upper (GtkAdjustment *adjustment,
454 : : GParamSpec *pspec,
455 : : ValentSmsConversation *self)
456 : : {
457 [ + - ]: 1 : if G_UNLIKELY (!gtk_widget_get_realized (GTK_WIDGET (self)))
458 : : return;
459 : :
460 : 1 : valent_sms_conversation_queue_update (self);
461 : : }
462 : :
463 : : static void
464 : 2 : on_thread_items_changed (GListModel *model,
465 : : unsigned int position,
466 : : unsigned int removed,
467 : : unsigned int added,
468 : : ValentSmsConversation *self)
469 : : {
470 : 2 : unsigned int position_upper, position_lower;
471 : 2 : unsigned int position_real;
472 : 2 : int diff;
473 : :
474 [ + - ]: 2 : g_assert (VALENT_IS_MESSAGE_THREAD (model));
475 [ - + ]: 2 : g_assert (VALENT_IS_SMS_CONVERSATION (self));
476 : :
477 : 2 : position_upper = self->position_upper;
478 : 2 : position_lower = self->position_lower;
479 : 2 : position_real = position_lower + position;
480 : :
481 : : /* First update the internal pointers */
482 : 2 : diff = added - removed;
483 : :
484 [ + - ]: 2 : if (position <= position_lower)
485 : 2 : self->position_lower += diff;
486 : :
487 [ + - ]: 2 : if (position <= position_upper)
488 : 2 : self->position_upper += diff;
489 : :
490 : : /* If the upper and lower are equal and we're being notified of additions,
491 : : * then this must be the initial load */
492 [ + - + - ]: 2 : if (self->position_lower == self->position_upper && added)
493 : : {
494 : 2 : valent_sms_conversation_queue_populate (self);
495 : 2 : return;
496 : : }
497 : :
498 : : /* If the position is in between our pointers we have to handle them */
499 [ # # ]: 0 : if (position >= position_lower && position <= position_upper)
500 : : {
501 : : /* Removals first */
502 [ # # ]: 0 : for (unsigned int i = 0; i < removed; i++)
503 : : {
504 : 0 : GtkListBoxRow *row;
505 : :
506 : 0 : row = gtk_list_box_get_row_at_index (self->message_list, position_real);
507 : 0 : gtk_list_box_remove (self->message_list, GTK_WIDGET (row));
508 : : }
509 : :
510 : : /* Additions */
511 [ # # ]: 0 : for (unsigned int i = 0; i < added; i++)
512 : : {
513 : 0 : g_autoptr (ValentMessage) message = NULL;
514 : :
515 : 0 : message = g_list_model_get_item (self->thread, position + i);
516 [ # # ]: 0 : valent_sms_conversation_insert_message (self, message, position_real + i);
517 : : }
518 : : }
519 : : }
520 : :
521 : : static void
522 : 4 : valent_sms_conversation_load (ValentSmsConversation *self)
523 : : {
524 [ + - + - ]: 4 : if (self->message_store == NULL || self->thread_id == self->loaded_id)
525 : : return;
526 : :
527 [ + + ]: 4 : if (!gtk_widget_get_mapped (GTK_WIDGET (self)))
528 : : return;
529 : :
530 : 2 : self->loaded_id = self->thread_id;
531 : 2 : self->thread = valent_sms_store_get_thread (self->message_store,
532 : : self->thread_id);
533 : 2 : g_signal_connect_object (self->thread,
534 : : "items-changed",
535 : : G_CALLBACK (on_thread_items_changed),
536 : : self, 0);
537 : : }
538 : :
539 : : static void
540 : 0 : valent_sms_conversation_send_message (ValentSmsConversation *self)
541 : : {
542 : 0 : g_autoptr (ValentMessage) message = NULL;
543 : 0 : GVariantBuilder builder, addresses;
544 : 0 : GHashTableIter iter;
545 : 0 : gpointer address;
546 : 0 : int sub_id = -1;
547 : 0 : const char *text;
548 : 0 : gboolean sent;
549 : :
550 [ # # ]: 0 : g_assert (VALENT_IS_SMS_CONVERSATION (self));
551 : :
552 : 0 : text = gtk_editable_get_text (GTK_EDITABLE (self->message_entry));
553 : :
554 [ # # # # ]: 0 : if (text == NULL || *text == '\0')
555 : 0 : return;
556 : :
557 : : // Metadata
558 : 0 : g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
559 : :
560 : : // Addresses
561 : 0 : g_variant_builder_init (&addresses, G_VARIANT_TYPE_ARRAY);
562 : 0 : g_hash_table_iter_init (&iter, self->participants);
563 : :
564 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, &address, NULL))
565 : 0 : g_variant_builder_add_parsed (&addresses, "{'address': <%s>}", address);
566 : :
567 : 0 : g_variant_builder_add (&builder, "{sv}", "addresses",
568 : : g_variant_builder_end (&addresses));
569 : :
570 : :
571 : : // TODO: SIM Card
572 : 0 : g_variant_builder_add (&builder, "{sv}", "sub_id",
573 : : g_variant_new_int64 (sub_id));
574 : :
575 : 0 : message = g_object_new (VALENT_TYPE_MESSAGE,
576 : : "box", VALENT_MESSAGE_BOX_OUTBOX,
577 : : "date", 0,
578 : : "id", -1,
579 : : "metadata", g_variant_builder_end (&builder),
580 : : "read", FALSE,
581 : : "sender", NULL,
582 : : "text", text,
583 : : "thread-id", self->thread_id,
584 : : NULL);
585 : :
586 : 0 : g_signal_emit (G_OBJECT (self), signals [SEND_MESSAGE], 0, message, &sent);
587 : :
588 [ # # ]: 0 : if (sent)
589 : : VALENT_NOTE ("TODO: Add pending message to conversation");
590 : : else
591 : 0 : g_warning ("%s(): failed sending message \"%s\"", G_STRFUNC, text);
592 : :
593 : : /* Clear the entry whether we failed or not */
594 [ # # ]: 0 : gtk_editable_set_text (GTK_EDITABLE (self->message_entry), "");
595 : : }
596 : :
597 : :
598 : : /*
599 : : * GtkWidget
600 : : */
601 : : static void
602 : 2 : valent_sms_conversation_map (GtkWidget *widget)
603 : : {
604 : 2 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (widget);
605 : :
606 : 2 : GTK_WIDGET_CLASS (valent_sms_conversation_parent_class)->map (widget);
607 : :
608 : 2 : gtk_widget_grab_focus (self->message_entry);
609 : 2 : valent_sms_conversation_load (self);
610 : 2 : }
611 : :
612 : :
613 : : /*
614 : : * GObject
615 : : */
616 : : static void
617 : 2 : valent_sms_conversation_dispose (GObject *object)
618 : : {
619 : 2 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (object);
620 : :
621 [ + - ]: 2 : if (self->thread != NULL)
622 : : {
623 : 2 : g_signal_handlers_disconnect_by_data (self->thread, self);
624 [ + - ]: 2 : g_clear_object (&self->thread);
625 : : }
626 : :
627 : 2 : gtk_widget_dispose_template (GTK_WIDGET (object),
628 : : VALENT_TYPE_SMS_CONVERSATION);
629 : :
630 : 2 : G_OBJECT_CLASS (valent_sms_conversation_parent_class)->dispose (object);
631 : 2 : }
632 : :
633 : : static void
634 : 2 : valent_sms_conversation_finalize (GObject *object)
635 : : {
636 : 2 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (object);
637 : :
638 [ + - ]: 2 : g_clear_object (&self->message_store);
639 [ + - ]: 2 : g_clear_object (&self->contact_store);
640 [ + - ]: 2 : g_clear_pointer (&self->participants, g_hash_table_unref);
641 [ + + ]: 2 : g_clear_pointer (&self->title, g_free);
642 [ - + ]: 2 : g_clear_pointer (&self->subtitle, g_free);
643 : :
644 : 2 : G_OBJECT_CLASS (valent_sms_conversation_parent_class)->finalize (object);
645 : 2 : }
646 : :
647 : : static void
648 : 3 : valent_sms_conversation_get_property (GObject *object,
649 : : guint prop_id,
650 : : GValue *value,
651 : : GParamSpec *pspec)
652 : : {
653 : 3 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (object);
654 : :
655 [ + + + - ]: 3 : switch (prop_id)
656 : : {
657 : 1 : case PROP_CONTACT_STORE:
658 : 1 : g_value_set_object (value, self->contact_store);
659 : 1 : break;
660 : :
661 : 1 : case PROP_MESSAGE_STORE:
662 : 1 : g_value_set_object (value, self->message_store);
663 : 1 : break;
664 : :
665 : 1 : case PROP_THREAD_ID:
666 : 1 : g_value_set_int64 (value, self->thread_id);
667 : 1 : break;
668 : :
669 : 0 : default:
670 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
671 : : }
672 : 3 : }
673 : :
674 : : static void
675 : 6 : valent_sms_conversation_set_property (GObject *object,
676 : : guint prop_id,
677 : : const GValue *value,
678 : : GParamSpec *pspec)
679 : : {
680 : 6 : ValentSmsConversation *self = VALENT_SMS_CONVERSATION (object);
681 : :
682 [ + + + - ]: 6 : switch (prop_id)
683 : : {
684 : 2 : case PROP_CONTACT_STORE:
685 : 2 : self->contact_store = g_value_dup_object (value);
686 : 2 : break;
687 : :
688 : 2 : case PROP_MESSAGE_STORE:
689 : 2 : self->message_store = g_value_dup_object (value);
690 : 2 : break;
691 : :
692 : 2 : case PROP_THREAD_ID:
693 : 2 : valent_sms_conversation_set_thread_id (self, g_value_get_int64 (value));
694 : 2 : break;
695 : :
696 : 0 : default:
697 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
698 : : }
699 : 6 : }
700 : :
701 : : static void
702 : 2 : valent_sms_conversation_class_init (ValentSmsConversationClass *klass)
703 : : {
704 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
705 : 2 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
706 : :
707 : 2 : object_class->dispose = valent_sms_conversation_dispose;
708 : 2 : object_class->finalize = valent_sms_conversation_finalize;
709 : 2 : object_class->get_property = valent_sms_conversation_get_property;
710 : 2 : object_class->set_property = valent_sms_conversation_set_property;
711 : :
712 : 2 : widget_class->map = valent_sms_conversation_map;
713 : :
714 : 2 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/sms/valent-sms-conversation.ui");
715 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsConversation, message_list);
716 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsConversation, message_entry);
717 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsConversation, message_view);
718 : 2 : gtk_widget_class_bind_template_child (widget_class, ValentSmsConversation, pending);
719 : :
720 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_edge_overshot);
721 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_entry_activated);
722 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_entry_changed);
723 : 2 : gtk_widget_class_bind_template_callback (widget_class, on_entry_icon_release);
724 : :
725 : 2 : gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_GRID_LAYOUT);
726 : :
727 : : /**
728 : : * ValentSmsConversation:contact-store:
729 : : *
730 : : * The `ValentContactStore` providing `EContact` objects for the conversation.
731 : : */
732 : 4 : properties [PROP_CONTACT_STORE] =
733 : 2 : g_param_spec_object ("contact-store", NULL, NULL,
734 : : VALENT_TYPE_CONTACT_STORE,
735 : : (G_PARAM_READWRITE |
736 : : G_PARAM_CONSTRUCT |
737 : : G_PARAM_EXPLICIT_NOTIFY |
738 : : G_PARAM_STATIC_STRINGS));
739 : :
740 : : /**
741 : : * ValentSmsConversation:message-store:
742 : : *
743 : : * The `ValentSmsStore` providing `ValentMessage` objects for the
744 : : * conversation.
745 : : */
746 : 4 : properties [PROP_MESSAGE_STORE] =
747 : 2 : g_param_spec_object ("message-store", NULL, NULL,
748 : : VALENT_TYPE_SMS_STORE,
749 : : (G_PARAM_READWRITE |
750 : : G_PARAM_CONSTRUCT_ONLY |
751 : : G_PARAM_EXPLICIT_NOTIFY |
752 : : G_PARAM_STATIC_STRINGS));
753 : :
754 : : /**
755 : : * ValentSmsConversation:thread-id:
756 : : *
757 : : * The thread ID of the conversation.
758 : : */
759 : 4 : properties [PROP_THREAD_ID] =
760 : 2 : g_param_spec_int64 ("thread-id", NULL, NULL,
761 : : G_MININT64, G_MAXINT64,
762 : : 0,
763 : : (G_PARAM_READWRITE |
764 : : G_PARAM_CONSTRUCT |
765 : : G_PARAM_EXPLICIT_NOTIFY |
766 : : G_PARAM_STATIC_STRINGS));
767 : :
768 : 2 : g_object_class_install_properties (object_class, N_PROPERTIES, properties);
769 : :
770 : : /**
771 : : * ValentSmsConversation::send-message:
772 : : * @conversation: a `ValentSmsConversation`
773 : : * @message: a message
774 : : *
775 : : * The `ValentSmsConversation`::send-message signal is emitted when a user is
776 : : * sending an outgoing message.
777 : : *
778 : : * The signal handler should return a boolean indicating success, although
779 : : * this only indicates the request was sent to the device.
780 : : */
781 : 4 : signals [SEND_MESSAGE] =
782 : 2 : g_signal_new ("send-message",
783 : : G_TYPE_FROM_CLASS (klass),
784 : : G_SIGNAL_RUN_LAST,
785 : : 0,
786 : : g_signal_accumulator_first_wins, NULL, NULL,
787 : : G_TYPE_BOOLEAN, 1, VALENT_TYPE_MESSAGE);
788 : 2 : }
789 : :
790 : : static void
791 : 2 : valent_sms_conversation_init (ValentSmsConversation *self)
792 : : {
793 : 2 : GtkScrolledWindow *scrolled;
794 : :
795 : 2 : gtk_widget_init_template (GTK_WIDGET (self));
796 : :
797 : : /* Watch the scroll position */
798 : 2 : scrolled = GTK_SCROLLED_WINDOW (self->message_view);
799 : 2 : self->vadjustment = gtk_scrolled_window_get_vadjustment (scrolled);
800 : 2 : g_signal_connect_after (self->vadjustment,
801 : : "notify::upper",
802 : : G_CALLBACK (on_scroll_notify_upper),
803 : : self);
804 : :
805 : 2 : gtk_list_box_set_header_func (self->message_list,
806 : : message_list_header_func,
807 : : self, NULL);
808 : :
809 : 2 : self->participants = g_hash_table_new_full (g_str_hash, g_str_equal,
810 : : g_free, g_object_unref);
811 : 2 : }
812 : :
813 : : GtkWidget *
814 : 0 : valent_sms_conversation_new (ValentContactStore *contacts,
815 : : ValentSmsStore *messages)
816 : : {
817 : 0 : return g_object_new (VALENT_TYPE_SMS_CONVERSATION,
818 : : "contact-store", contacts,
819 : : "message-store", messages,
820 : : NULL);
821 : : }
822 : :
823 : : /**
824 : : * valent_sms_conversation_get_thread_id:
825 : : * @conversation: a `ValentSmsConversation`
826 : : *
827 : : * Get the thread ID for @conversation.
828 : : *
829 : : * Returns: the thread ID
830 : : */
831 : : int64_t
832 : 1 : valent_sms_conversation_get_thread_id (ValentSmsConversation *conversation)
833 : : {
834 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION (conversation), 0);
835 : :
836 : 1 : return conversation->thread_id;
837 : : }
838 : :
839 : : /**
840 : : * valent_sms_conversation_set_thread_id:
841 : : * @conversation: a `ValentSmsConversation`
842 : : * @thread_id: a thread ID
843 : : *
844 : : * Set the thread ID for @conversation.
845 : : */
846 : : void
847 : 2 : valent_sms_conversation_set_thread_id (ValentSmsConversation *conversation,
848 : : int64_t thread_id)
849 : : {
850 : 2 : GtkWidget *parent = GTK_WIDGET (conversation->message_list);
851 : 2 : GtkWidget *child;
852 : :
853 [ + - ]: 2 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION (conversation));
854 [ - + ]: 2 : g_return_if_fail (thread_id >= 0);
855 : :
856 [ + - ]: 2 : if (conversation->thread_id == thread_id)
857 : : return;
858 : :
859 : : /* Clear the current messages */
860 [ - + ]: 2 : if (conversation->thread != NULL)
861 : : {
862 : 0 : g_signal_handlers_disconnect_by_data (conversation->thread, conversation);
863 [ # # ]: 0 : g_clear_object (&conversation->thread);
864 : : }
865 : :
866 [ + + ]: 4 : while ((child = gtk_widget_get_first_child (parent)))
867 : 2 : gtk_list_box_remove (conversation->message_list, child);
868 : :
869 : : /* Notify before beginning the load task */
870 : 2 : conversation->thread_id = thread_id;
871 : 2 : g_object_notify_by_pspec (G_OBJECT (conversation), properties [PROP_THREAD_ID]);
872 : :
873 : : /* Load the new thread */
874 : 2 : valent_sms_conversation_load (conversation);
875 : : }
876 : :
877 : : /**
878 : : * valent_sms_conversation_get_title:
879 : : * @conversation: a `ValentSmsConversation`
880 : : *
881 : : * Get the title of the conversation, usually the contact name.
882 : : *
883 : : * Returns: (transfer none): the conversation title
884 : : */
885 : : const char *
886 : 1 : valent_sms_conversation_get_title (ValentSmsConversation *conversation)
887 : : {
888 : 2 : g_autofree char **addresses = NULL;
889 : 1 : g_autoptr (GList) contacts = NULL;
890 : 1 : unsigned int n_contacts = 0;
891 : :
892 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION (conversation), NULL);
893 : :
894 [ + - ]: 1 : if (conversation->title == NULL)
895 : : {
896 [ - + ]: 1 : g_clear_pointer (&conversation->subtitle, g_free);
897 : :
898 : 1 : addresses = (char **)g_hash_table_get_keys_as_array (conversation->participants,
899 : : &n_contacts);
900 : 1 : contacts = g_hash_table_get_values (conversation->participants);
901 : :
902 [ + - ]: 1 : if (n_contacts == 0)
903 : : {
904 [ - + ]: 1 : conversation->title = g_strdup (_("New Conversation"));
905 : 1 : conversation->subtitle = NULL;
906 : : }
907 : : else
908 : : {
909 : 0 : conversation->title = e_contact_get (contacts->data,
910 : : E_CONTACT_FULL_NAME);
911 : :
912 [ # # ]: 0 : if (n_contacts == 1)
913 : : {
914 [ # # ]: 0 : conversation->subtitle = g_strdup ((const char *)addresses[0]);
915 : : }
916 : : else
917 : : {
918 : 0 : unsigned int remainder;
919 : :
920 : 0 : remainder = n_contacts - 1;
921 : 0 : conversation->subtitle = g_strdup_printf (ngettext ("%u other contact",
922 : : "%u others",
923 : : remainder),
924 : : remainder);
925 : : }
926 : : }
927 : : }
928 : :
929 [ + - ]: 1 : return conversation->title;
930 : : }
931 : :
932 : : /**
933 : : * valent_sms_conversation_get_subtitle:
934 : : * @conversation: a `ValentSmsConversation`
935 : : *
936 : : * Get the subtitle of the conversation. If the conversation has one recipient
937 : : * this will be its address (eg. phone number), otherwise it will be a string
938 : : * such as "And 2 others".
939 : : *
940 : : * Returns: (transfer none): the conversation subtitle
941 : : */
942 : : const char *
943 : 0 : valent_sms_conversation_get_subtitle (ValentSmsConversation *conversation)
944 : : {
945 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION (conversation), NULL);
946 : :
947 [ # # ]: 0 : if (conversation->title == NULL)
948 : 0 : valent_sms_conversation_get_title (conversation);
949 : :
950 : 0 : return conversation->subtitle;
951 : : }
952 : :
953 : : /**
954 : : * valent_sms_conversation_scroll_to_date:
955 : : * @conversation: a `ValentSmsConversation`
956 : : * @date: a UNIX epoch timestamp
957 : : *
958 : : * Scroll to the message closest to @date.
959 : : */
960 : : void
961 : 0 : valent_sms_conversation_scroll_to_date (ValentSmsConversation *conversation,
962 : : int64_t date)
963 : : {
964 : 0 : GtkWidget *row;
965 : 0 : ValentMessage *message;
966 : :
967 [ # # ]: 0 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION (conversation));
968 [ # # ]: 0 : g_return_if_fail (date > 0);
969 : :
970 : : /* First look through the list box */
971 : 0 : for (row = gtk_widget_get_last_child (GTK_WIDGET (conversation->message_list));
972 [ # # ]: 0 : row != NULL;
973 : 0 : row = gtk_widget_get_prev_sibling (row))
974 : : {
975 [ # # ]: 0 : if G_UNLIKELY (GTK_LIST_BOX_ROW (row) == conversation->pending)
976 : 0 : continue;
977 : :
978 : : /* If this message is equal or older than the target date, we're done */
979 [ # # ]: 0 : if (valent_sms_conversation_row_get_date (VALENT_SMS_CONVERSATION_ROW (row)) <= date)
980 : : {
981 : 0 : valent_sms_conversation_scroll_to_row (conversation, row);
982 : 0 : return;
983 : : }
984 : : }
985 : :
986 : : /* If there are no more messages, we're done */
987 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGE_THREAD (conversation->thread));
988 : :
989 : : /* Populate the list in reverse until we find the message */
990 [ # # ]: 0 : while ((message = valent_sms_conversation_pop_tail (conversation)) != NULL)
991 : : {
992 : : /* Prepend the message */
993 : 0 : row = valent_sms_conversation_insert_message (conversation, message, 0);
994 : 0 : g_object_unref (message);
995 : :
996 : : /* If this message is equal or older than the target date, we're done */
997 [ # # ]: 0 : if (valent_message_get_date (message) <= date)
998 : : {
999 : 0 : valent_sms_conversation_scroll_to_row (conversation, row);
1000 : 0 : return;
1001 : : }
1002 : : }
1003 : : }
1004 : :
1005 : : /**
1006 : : * valent_sms_conversation_scroll_to_message:
1007 : : * @conversation: a `ValentSmsConversation`
1008 : : * @message: a `ValentMessage`
1009 : : *
1010 : : * A convenience for calling valent_message_get_date() and then
1011 : : * valent_sms_conversation_scroll_to_date().
1012 : : */
1013 : : void
1014 : 0 : valent_sms_conversation_scroll_to_message (ValentSmsConversation *conversation,
1015 : : ValentMessage *message)
1016 : : {
1017 : 0 : int64_t date;
1018 : :
1019 [ # # ]: 0 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION (conversation));
1020 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGE (message));
1021 : :
1022 : 0 : date = valent_message_get_date (message);
1023 : 0 : valent_sms_conversation_scroll_to_date (conversation, date);
1024 : : }
1025 : :
|