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-conversation-page"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gtk/gtk.h>
10 : : #include <libebook-contacts/libebook-contacts.h>
11 : : #include <libtracker-sparql/tracker-sparql.h>
12 : : #include <valent.h>
13 : :
14 : : #include "valent-date-label.h"
15 : : #include "valent-contact-page.h"
16 : : #include "valent-contact-row.h"
17 : : #include "valent-conversation-row.h"
18 : : #include "valent-ui-utils-private.h"
19 : :
20 : : #include "valent-conversation-page.h"
21 : :
22 : : #define GET_THREAD_ATTACHMENTS_RQ "/ca/andyholmes/Valent/sparql/get-thread-attachments.rq"
23 : :
24 : : struct _ValentConversationPage
25 : : {
26 : : AdwNavigationPage parent_instance;
27 : :
28 : : GListModel *contacts;
29 : : ValentMessagesAdapter *messages;
30 : : char *iri;
31 : : GListModel *thread;
32 : : TrackerSparqlStatement *get_thread_attachments_stmt;
33 : : GHashTable *participants;
34 : : GHashTable *outbox;
35 : : GListStore *attachments;
36 : :
37 : : /* Viewport state */
38 : : double offset;
39 : : unsigned int position_bottom;
40 : : unsigned int position_top;
41 : : gboolean should_scroll;
42 : : unsigned int populate_id;
43 : : unsigned int update_id;
44 : :
45 : : /* template */
46 : : GtkScrolledWindow *scrolledwindow;
47 : : GtkAdjustment *vadjustment;
48 : : GtkListBox *message_list;
49 : : GtkWidget *message_entry;
50 : :
51 : : AdwDialog *details_dialog;
52 : : AdwNavigationView *details_view;
53 : : GtkWidget *participant_list;
54 : : GtkWidget *attachment_list;
55 : : };
56 : :
57 : : static void valent_conversation_page_announce_message (ValentConversationPage *self,
58 : : ValentMessage *message);
59 : : static gboolean valent_conversation_page_check_message (ValentConversationPage *self);
60 : : static void valent_conversation_page_send_message (ValentConversationPage *self);
61 : :
62 [ + + + - ]: 6 : G_DEFINE_FINAL_TYPE (ValentConversationPage, valent_conversation_page, ADW_TYPE_NAVIGATION_PAGE)
63 : :
64 : : typedef enum {
65 : : PROP_CONTACTS = 1,
66 : : PROP_MESSAGES,
67 : : PROP_IRI,
68 : : } ValentConversationPageProperty;
69 : :
70 : : static GParamSpec *properties[PROP_IRI + 1] = { NULL, };
71 : :
72 : :
73 : : static void
74 : 0 : phone_lookup_cb (ValentContactsAdapter *adapter,
75 : : GAsyncResult *result,
76 : : GtkWidget *widget)
77 : : {
78 : 0 : g_autoptr (EContact) contact = NULL;
79 : 0 : g_autoptr (GError) error = NULL;
80 : 0 : GtkWidget *conversation;
81 : :
82 : 0 : contact = valent_contacts_adapter_reverse_lookup_finish (adapter, result, &error);
83 [ # # ]: 0 : if (contact == NULL)
84 : : {
85 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
86 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
87 : :
88 [ # # ]: 0 : return;
89 : : }
90 : :
91 : 0 : conversation = gtk_widget_get_ancestor (widget, VALENT_TYPE_CONVERSATION_PAGE);
92 [ # # ]: 0 : if (conversation != NULL)
93 : : {
94 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (conversation);
95 : 0 : ValentConversationRow *row = VALENT_CONVERSATION_ROW (widget);
96 : 0 : ValentMessage *message = valent_conversation_row_get_message (row);
97 : 0 : const char *sender = valent_message_get_sender (message);
98 : :
99 : 0 : valent_conversation_page_add_participant (self, contact, sender);
100 : 0 : valent_conversation_row_set_contact (row, contact);
101 : : }
102 : : }
103 : :
104 : : static void
105 : 0 : message_list_header_func (GtkListBoxRow *row,
106 : : GtkListBoxRow *before,
107 : : gpointer user_data)
108 : : {
109 : 0 : ValentConversationRow *current_row = VALENT_CONVERSATION_ROW (row);
110 : 0 : ValentConversationRow *prev_row = VALENT_CONVERSATION_ROW (before);
111 : 0 : int64_t row_date, prev_date;
112 : 0 : gboolean row_incoming;
113 : :
114 [ # # # # : 0 : g_assert (GTK_IS_LIST_BOX_ROW (row));
# # # # ]
115 [ # # # # : 0 : g_assert (before == NULL || GTK_IS_LIST_BOX_ROW (before));
# # # # ]
116 : :
117 : : /* If this is an incoming message, show the avatar
118 : : */
119 : 0 : row_incoming = valent_conversation_row_is_incoming (current_row);
120 : 0 : valent_conversation_row_show_avatar (current_row, row_incoming);
121 : :
122 [ # # ]: 0 : if (before == NULL)
123 : : return;
124 : :
125 : : /* If it's been more than an hour between messages, show a date label.
126 : : * Otherwise, if the current and previous rows are incoming, hide the
127 : : * previous row's avatar.
128 : : */
129 : 0 : prev_date = valent_conversation_row_get_date (prev_row);
130 : 0 : row_date = valent_conversation_row_get_date (current_row);
131 [ # # ]: 0 : if (row_date - prev_date > G_TIME_SPAN_HOUR / 1000)
132 : : {
133 : 0 : GtkWidget *header = gtk_list_box_row_get_header (row);
134 : :
135 [ # # ]: 0 : if (header == NULL)
136 : : {
137 : 0 : header = g_object_new (VALENT_TYPE_DATE_LABEL,
138 : : "date", row_date,
139 : : "mode", VALENT_DATE_FORMAT_ADAPTIVE,
140 : : NULL);
141 : 0 : gtk_widget_add_css_class (header, "date-marker");
142 : 0 : gtk_widget_add_css_class (header, "dim-label");
143 : 0 : gtk_list_box_row_set_header (row, header);
144 : : }
145 : : }
146 [ # # ]: 0 : else if (valent_conversation_row_is_incoming (prev_row))
147 : : {
148 : 0 : valent_conversation_row_show_avatar (prev_row, !row_incoming);
149 : : }
150 : : }
151 : :
152 : : /*< private >
153 : : * valent_conversation_page_insert_message:
154 : : * @conversation: a `ValentConversationPage`
155 : : * @message: a `ValentMessage`
156 : : * @position: position to insert the widget
157 : : *
158 : : * Create a new message row for @message and insert it into the message list at
159 : : * @position.
160 : : *
161 : : * Returns: (transfer none): a `GtkWidget`
162 : : */
163 : : static GtkWidget *
164 : 0 : valent_conversation_page_insert_message (ValentConversationPage *self,
165 : : ValentMessage *message,
166 : : int position)
167 : : {
168 : 0 : ValentConversationRow *row;
169 : 0 : const char *medium = NULL;
170 : :
171 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
172 [ # # ]: 0 : g_assert (VALENT_IS_MESSAGE (message));
173 : :
174 : 0 : row = g_object_new (VALENT_TYPE_CONVERSATION_ROW,
175 : : "message", message,
176 : : "activatable", FALSE,
177 : : "selectable", FALSE,
178 : : NULL);
179 : :
180 : 0 : medium = valent_message_get_sender (message);
181 [ # # # # ]: 0 : if (medium != NULL && *medium != '\0')
182 : : {
183 : 0 : EContact *contact;
184 : :
185 : 0 : contact = g_hash_table_lookup (self->participants, medium);
186 [ # # ]: 0 : if (contact != NULL)
187 : : {
188 : 0 : valent_conversation_row_set_contact (row, contact);
189 : : }
190 : : else
191 : : {
192 : 0 : g_autoptr (GCancellable) cancellable = NULL;
193 : :
194 : 0 : cancellable = g_cancellable_new ();
195 : 0 : g_signal_connect_object (row,
196 : : "destroy",
197 : : G_CALLBACK (g_cancellable_cancel),
198 : : cancellable,
199 : : G_CONNECT_SWAPPED);
200 [ # # ]: 0 : valent_contacts_adapter_reverse_lookup ((gpointer)self->contacts,
201 : : medium,
202 : : cancellable,
203 : : (GAsyncReadyCallback)phone_lookup_cb,
204 : : row);
205 : : }
206 : : }
207 [ # # ]: 0 : else if (g_hash_table_size (self->participants) == 1)
208 : : {
209 : 0 : GHashTableIter iter;
210 : 0 : EContact *contact;
211 : :
212 : 0 : g_hash_table_iter_init (&iter, self->participants);
213 : 0 : g_hash_table_iter_next (&iter, NULL, (void **)&contact);
214 : 0 : valent_conversation_row_set_contact (row, contact);
215 : : }
216 : :
217 : 0 : gtk_list_box_insert (self->message_list, GTK_WIDGET (row), position);
218 : :
219 : 0 : return GTK_WIDGET (row);
220 : : }
221 : :
222 : : /*
223 : : * Scrolled Window
224 : : */
225 : : static ValentMessage *
226 : 0 : valent_conversation_page_pop_tail (ValentConversationPage *self)
227 : : {
228 : 0 : ValentMessage *ret = NULL;
229 : :
230 [ # # ]: 0 : g_assert (G_IS_LIST_MODEL (self->thread));
231 : :
232 [ # # ]: 0 : if (self->position_top > 0)
233 : : {
234 : 0 : self->position_top -= 1;
235 : 0 : ret = g_list_model_get_item (self->thread, self->position_top);
236 : : }
237 : :
238 : 0 : return ret;
239 : : }
240 : :
241 : : static void
242 : 0 : valent_conversation_page_populate_reverse (ValentConversationPage *self,
243 : : unsigned int count)
244 : : {
245 : 0 : unsigned int n_items;
246 : :
247 [ # # ]: 0 : if G_UNLIKELY (self->thread == NULL)
248 : : return;
249 : :
250 : 0 : n_items = g_list_model_get_n_items (self->thread);
251 [ # # ]: 0 : if (n_items == 0)
252 : : return;
253 : :
254 : : /* Prime the top position for the first message, so that result is the
255 : : * top and bottom positions equivalent to the number of messages.
256 : : */
257 [ # # ]: 0 : if (self->position_bottom == self->position_top)
258 : : {
259 : 0 : self->position_top = n_items;
260 : 0 : self->position_bottom = n_items - 1;
261 : : }
262 : :
263 [ # # ]: 0 : for (unsigned int i = 0; i < count; i++)
264 : : {
265 : 0 : g_autoptr (ValentMessage) message = NULL;
266 : :
267 : 0 : message = valent_conversation_page_pop_tail (self);
268 [ # # ]: 0 : if (message == NULL)
269 : : break;
270 : :
271 : 0 : valent_conversation_page_insert_message (self, message, 0);
272 : : }
273 : :
274 : 0 : gtk_list_box_invalidate_headers (self->message_list);
275 : : }
276 : :
277 : : static gboolean
278 : 0 : valent_conversation_page_populate (gpointer data)
279 : : {
280 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (data);
281 : 0 : double page_size = gtk_adjustment_get_page_size (self->vadjustment);
282 : 0 : double upper = gtk_adjustment_get_upper (self->vadjustment);
283 : 0 : double value = gtk_adjustment_get_value (self->vadjustment);
284 : :
285 : 0 : self->offset = (upper - page_size) - value;
286 : 0 : self->should_scroll = TRUE;
287 : :
288 : 0 : valent_conversation_page_populate_reverse (self, 25);
289 : 0 : self->populate_id = 0;
290 : :
291 : 0 : return G_SOURCE_REMOVE;
292 : : }
293 : :
294 : : static inline void
295 : 0 : valent_conversation_page_queue_populate (ValentConversationPage *self)
296 : : {
297 [ # # ]: 0 : if (self->populate_id == 0)
298 : : {
299 : 0 : self->populate_id = g_idle_add_full (G_PRIORITY_LOW,
300 : : valent_conversation_page_populate,
301 : : g_object_ref (self),
302 : : g_object_unref);
303 : : }
304 : 0 : }
305 : :
306 : : static gboolean
307 : 1 : valent_conversation_page_update (gpointer data)
308 : : {
309 : 1 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (data);
310 : 1 : double page_size = gtk_adjustment_get_page_size (self->vadjustment);
311 : :
312 [ - + ]: 1 : if (self->should_scroll)
313 : : {
314 : 0 : double upper = gtk_adjustment_get_upper (self->vadjustment);
315 : 0 : double new_value = (upper - page_size) - self->offset;
316 : :
317 : 0 : self->offset = 0;
318 : 0 : self->should_scroll = FALSE;
319 : 0 : gtk_adjustment_set_value (self->vadjustment, new_value);
320 : : }
321 : :
322 : 1 : self->update_id = 0;
323 : :
324 : 1 : return G_SOURCE_REMOVE;
325 : : }
326 : :
327 : : static inline void
328 : 1 : valent_conversation_page_queue_update (ValentConversationPage *self)
329 : : {
330 [ + - ]: 1 : if (self->update_id == 0)
331 : : {
332 : 1 : self->update_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
333 : : valent_conversation_page_update,
334 : : g_object_ref (self),
335 : : g_object_unref);
336 : : }
337 : 1 : }
338 : :
339 : : static void
340 : 1 : on_scroll_upper_changed (ValentConversationPage *self)
341 : : {
342 [ + - ]: 1 : if G_UNLIKELY (!gtk_widget_get_realized (GTK_WIDGET (self)))
343 : : return;
344 : :
345 : 1 : valent_conversation_page_queue_update (self);
346 : : }
347 : :
348 : : static void
349 : 0 : on_scroll_value_changed (ValentConversationPage *self)
350 : : {
351 : 0 : double page_size = gtk_adjustment_get_page_size (self->vadjustment);
352 : 0 : double value = gtk_adjustment_get_value (self->vadjustment);
353 : :
354 [ # # ]: 0 : if (value < (page_size * 2))
355 : 0 : valent_conversation_page_queue_populate (self);
356 : 0 : }
357 : :
358 : : static gboolean
359 : 0 : valent_conversation_page_is_latest (ValentConversationPage *self)
360 : : {
361 : 0 : double upper, value, page_size;
362 : :
363 : 0 : value = gtk_adjustment_get_value (self->vadjustment);
364 : 0 : upper = gtk_adjustment_get_upper (self->vadjustment);
365 : 0 : page_size = gtk_adjustment_get_page_size (self->vadjustment);
366 : :
367 [ # # ]: 0 : return ABS (upper - page_size - value) <= DBL_EPSILON;
368 : : }
369 : :
370 : : static void
371 : 0 : valent_conversation_page_announce_message (ValentConversationPage *self,
372 : : ValentMessage *message)
373 : : {
374 : 0 : g_autofree char *summary = NULL;
375 : 0 : GListModel *attachments;
376 : 0 : unsigned int n_attachments = 0;
377 : 0 : EContact *contact = NULL;
378 : 0 : const char *contact_medium = NULL;
379 : 0 : const char *sender = NULL;
380 : 0 : const char *text = NULL;
381 : :
382 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
383 [ # # ]: 0 : g_assert (VALENT_IS_MESSAGE (message));
384 : :
385 [ # # ]: 0 : if (valent_message_get_box (message) != VALENT_MESSAGE_BOX_INBOX)
386 : 0 : return;
387 : :
388 : 0 : attachments = valent_message_get_attachments (message);
389 [ # # ]: 0 : if (attachments != NULL)
390 : 0 : n_attachments = g_list_model_get_n_items (attachments);
391 : :
392 : 0 : contact_medium = valent_message_get_sender (message);
393 [ # # # # ]: 0 : if (contact_medium != NULL && *contact_medium != '\0')
394 : 0 : contact = g_hash_table_lookup (self->participants, contact_medium);
395 : :
396 [ # # # # ]: 0 : if (contact == NULL && g_hash_table_size (self->participants) == 1)
397 : : {
398 : 0 : GHashTableIter iter;
399 : :
400 : 0 : g_hash_table_iter_init (&iter, self->participants);
401 : 0 : g_hash_table_iter_next (&iter, (void **)&sender, (void **)&contact);
402 : : }
403 : :
404 [ # # ]: 0 : if (contact != NULL)
405 : 0 : sender = e_contact_get_const (contact, E_CONTACT_FULL_NAME);
406 [ # # ]: 0 : else if (sender == NULL)
407 : 0 : sender = _("Unknown");
408 : :
409 [ # # ]: 0 : if (n_attachments == 0)
410 : : {
411 : : /* TRANSLATORS: This is announced to AT devices (i.e. screen readers)
412 : : * when a new message is received.
413 : : */
414 : 0 : summary = g_strdup_printf (_("New message from %s"), sender);
415 : : }
416 : : else
417 : : {
418 : : /* TRANSLATORS: This is announced to AT devices (i.e. screen readers)
419 : : * when a new message is received with attachments.
420 : : */
421 : 0 : summary = g_strdup_printf (ngettext ("New message from %s, with %d attachment",
422 : : "New message from %s, with %d attachments",
423 : : n_attachments),
424 : : sender, n_attachments);
425 : : }
426 : :
427 : 0 : gtk_accessible_announce (GTK_ACCESSIBLE (self),
428 : : summary,
429 : : GTK_ACCESSIBLE_ANNOUNCEMENT_PRIORITY_MEDIUM);
430 : :
431 : : // TODO: should the summary be different if the message has no text content?
432 : 0 : text = valent_message_get_text (message);
433 [ # # # # ]: 0 : if (text != NULL && *text != '\0')
434 : : {
435 : 0 : gtk_accessible_announce (GTK_ACCESSIBLE (self),
436 : : text,
437 : : GTK_ACCESSIBLE_ANNOUNCEMENT_PRIORITY_MEDIUM);
438 : : }
439 : : }
440 : :
441 : : static gboolean
442 : 0 : valent_conversation_page_clear_outbox (ValentConversationPage *self,
443 : : ValentMessage *message)
444 : : {
445 : 0 : GHashTableIter iter;
446 : 0 : ValentMessage *expected;
447 : 0 : GtkWidget *row;
448 : :
449 [ # # ]: 0 : if (valent_message_get_box (message) != VALENT_MESSAGE_BOX_SENT)
450 : : return FALSE;
451 : :
452 : 0 : g_hash_table_iter_init (&iter, self->outbox);
453 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&row, (void **)&expected))
454 : : {
455 : 0 : const char *text = valent_message_get_text (message);
456 : 0 : const char *expected_text = valent_message_get_text (expected);
457 : 0 : GListModel *attachments;
458 : 0 : GListModel *expected_attachments;
459 : 0 : unsigned int n_attachments = 0;
460 : 0 : unsigned int n_expected_attachments = 0;
461 : :
462 : : /* TODO: Normalizing NULL and the empty string might not be the right
463 : : * thing to do.
464 : : */
465 [ # # ]: 0 : text = text != NULL ? text : "";
466 [ # # ]: 0 : expected_text = expected_text != NULL ? expected_text : "";
467 [ # # ]: 0 : if (!g_str_equal (text, expected_text))
468 : 0 : continue;
469 : :
470 : : /* TODO: This check should compare the attachments, but it's not terribly
471 : : * likely there will be a conflict here.
472 : : */
473 : 0 : attachments = valent_message_get_attachments (message);
474 [ # # ]: 0 : if (attachments != NULL)
475 : 0 : n_attachments = g_list_model_get_n_items (attachments);
476 : :
477 : 0 : expected_attachments = valent_message_get_attachments (expected);
478 [ # # ]: 0 : if (expected_attachments != NULL)
479 : 0 : n_expected_attachments = g_list_model_get_n_items (expected_attachments);
480 : :
481 [ # # ]: 0 : if (n_attachments != n_expected_attachments)
482 : 0 : continue;
483 : :
484 : 0 : g_hash_table_iter_remove (&iter);
485 : 0 : gtk_list_box_remove (self->message_list, row);
486 : :
487 : 0 : return TRUE;
488 : : }
489 : :
490 : : return FALSE;
491 : : }
492 : :
493 : : static void
494 : 0 : on_thread_items_changed (GListModel *model,
495 : : unsigned int position,
496 : : unsigned int removed,
497 : : unsigned int added,
498 : : ValentConversationPage *self)
499 : : {
500 : 0 : unsigned int position_bottom, position_top;
501 : 0 : unsigned int position_real;
502 : :
503 [ # # ]: 0 : g_assert (G_IS_LIST_MODEL (model));
504 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
505 : :
506 : : /* If the top and bottom positions are equal and we're being notified of
507 : : * additions, then this must be the initial load
508 : : */
509 [ # # # # ]: 0 : if (self->position_top == self->position_bottom && added > 0)
510 : : {
511 : 0 : valent_conversation_page_queue_populate (self);
512 : 0 : return;
513 : : }
514 : :
515 : : /* Update the internal pointers that track the thread position at the top
516 : : * and bottom of the viewport canvas (i.e. loaded).
517 : : */
518 : 0 : position_bottom = self->position_bottom;
519 : 0 : position_top = self->position_top;
520 : 0 : position_real = position - position_top;
521 : :
522 [ # # ]: 0 : if (position <= position_top)
523 : 0 : self->position_top = position;
524 : :
525 [ # # ]: 0 : if (position >= position_bottom)
526 : : {
527 : 0 : self->position_bottom = position;
528 : 0 : self->should_scroll = valent_conversation_page_is_latest (self);
529 : : }
530 : :
531 : : /* Load the message if the position is greater than or equal to the top
532 : : * position, or if it's also higher than the bottom position (new message).
533 : : */
534 [ # # ]: 0 : if (position >= position_top)
535 : : {
536 [ # # ]: 0 : for (unsigned int i = 0; i < removed; i++)
537 : : {
538 : 0 : GtkListBoxRow *row;
539 : :
540 : 0 : row = gtk_list_box_get_row_at_index (self->message_list, position_real);
541 : 0 : gtk_list_box_remove (self->message_list, GTK_WIDGET (row));
542 : : }
543 : :
544 [ # # ]: 0 : for (unsigned int i = 0; i < added; i++)
545 : : {
546 : 0 : g_autoptr (ValentMessage) message = NULL;
547 : :
548 : 0 : message = g_list_model_get_item (self->thread, position + i);
549 : :
550 : : /* If this is new message, check if it matches an outbox row.
551 : : */
552 [ # # ]: 0 : if (position >= position_bottom)
553 : 0 : valent_conversation_page_clear_outbox (self, message);
554 : :
555 : 0 : valent_conversation_page_insert_message (self,
556 : : message,
557 : 0 : position_real + i);
558 : :
559 : : /* If this is new message, announce it for AT devices.
560 : : */
561 [ # # ]: 0 : if (position >= position_bottom)
562 : 0 : valent_conversation_page_announce_message (self, message);
563 : : }
564 : : }
565 : :
566 : 0 : gtk_list_box_invalidate_headers (self->message_list);
567 : : }
568 : :
569 : : static void
570 : 0 : valent_conversation_page_load (ValentConversationPage *self)
571 : : {
572 : 0 : unsigned int n_threads = 0;
573 : :
574 [ # # ]: 0 : if (self->messages == NULL)
575 : : return;
576 : :
577 : 0 : n_threads = g_list_model_get_n_items (G_LIST_MODEL (self->messages));
578 [ # # ]: 0 : for (unsigned int i = 0; i < n_threads; i++)
579 : : {
580 : 0 : g_autoptr (GListModel) thread = NULL;
581 [ # # # # ]: 0 : g_autofree char *thread_iri = NULL;
582 : :
583 : 0 : thread = g_list_model_get_item (G_LIST_MODEL (self->messages), i);
584 : 0 : g_object_get (thread, "iri", &thread_iri, NULL);
585 : :
586 [ # # ]: 0 : if (g_strcmp0 (self->iri, thread_iri) == 0)
587 : : {
588 : 0 : g_set_object (&self->thread, thread);
589 : 0 : break;
590 : : }
591 : : }
592 : :
593 [ # # ]: 0 : if (self->thread != NULL)
594 : : {
595 : 0 : g_signal_connect_object (self->thread,
596 : : "items-changed",
597 : : G_CALLBACK (on_thread_items_changed),
598 : : self,
599 : : G_CONNECT_DEFAULT);
600 : 0 : on_thread_items_changed (self->thread,
601 : : 0,
602 : : 0,
603 : : g_list_model_get_n_items (self->thread),
604 : : self);
605 : : }
606 : : }
607 : :
608 : : /*
609 : : * Message Entry
610 : : */
611 : : static void
612 : 0 : on_entry_activated (GtkEntry *entry,
613 : : ValentConversationPage *self)
614 : : {
615 : 0 : valent_conversation_page_send_message (self);
616 : 0 : }
617 : :
618 : : static void
619 : 0 : on_entry_changed (GtkEntry *entry,
620 : : ValentConversationPage *self)
621 : : {
622 : 0 : valent_conversation_page_check_message (self);
623 : 0 : }
624 : :
625 : : /*< private >
626 : : * valent_conversation_page_check_message:
627 : : * @self: a `ValentConversationPage`
628 : : *
629 : : * Send the current text and/or attachment provided by the user.
630 : : */
631 : : static gboolean
632 : 0 : valent_conversation_page_check_message (ValentConversationPage *self)
633 : : {
634 : 0 : const char *text;
635 : 0 : gboolean ready = FALSE;
636 : :
637 : 0 : text = gtk_editable_get_text (GTK_EDITABLE (self->message_entry));
638 [ # # # # : 0 : if (self->attachments != NULL || (text != NULL && *text != '\0'))
# # ]
639 : 0 : ready = TRUE;
640 : :
641 : 0 : gtk_widget_action_set_enabled (GTK_WIDGET (self), "message.send", ready);
642 : :
643 : 0 : return ready;
644 : : }
645 : :
646 : : static void
647 : 0 : valent_conversation_page_send_message_cb (ValentMessagesAdapter *adapter,
648 : : GAsyncResult *result,
649 : : gpointer user_data)
650 : : {
651 : 0 : g_autoptr (ValentConversationPage) self = VALENT_CONVERSATION_PAGE (g_steal_pointer (&user_data));
652 : 0 : GError *error = NULL;
653 : :
654 [ # # ]: 0 : if (valent_messages_adapter_send_message_finish (adapter, result, &error))
655 : : {
656 : 0 : ValentMessage *message = g_task_get_task_data (G_TASK (result));
657 : 0 : GtkWidget *row;
658 : :
659 : : /* Append and scroll to the outgoing message
660 : : */
661 : 0 : self->should_scroll = TRUE;
662 : 0 : row = g_object_new (VALENT_TYPE_CONVERSATION_ROW,
663 : : "message", message,
664 : : NULL);
665 : 0 : gtk_list_box_insert (GTK_LIST_BOX (self->message_list), row, -1);
666 : 0 : g_hash_table_replace (self->outbox,
667 : : g_object_ref (row),
668 : : g_object_ref (message));
669 : :
670 [ # # ]: 0 : g_clear_object (&self->attachments);
671 : 0 : gtk_editable_set_text (GTK_EDITABLE (self->message_entry), "");
672 : 0 : gtk_widget_remove_css_class (self->message_entry, "error");
673 : : }
674 [ # # ]: 0 : else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
675 : : {
676 : 0 : gtk_widget_add_css_class (self->message_entry, "error");
677 : : }
678 : 0 : }
679 : :
680 : : /*< private >
681 : : * valent_conversation_page_send_message:
682 : : * @self: a `ValentConversationPage`
683 : : *
684 : : * Send the current text and/or attachment provided by the user.
685 : : */
686 : : static void
687 : 0 : valent_conversation_page_send_message (ValentConversationPage *self)
688 : : {
689 : 0 : g_autoptr (ValentMessage) message = NULL;
690 [ # # ]: 0 : g_autoptr (GStrvBuilder) builder = NULL;
691 [ # # ]: 0 : g_auto (GStrv) recipients = NULL;
692 : 0 : GHashTableIter iter;
693 : 0 : const char *recipient;
694 : 0 : int64_t subscription_id;
695 : 0 : const char *text;
696 : :
697 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
698 : :
699 : 0 : text = gtk_editable_get_text (GTK_EDITABLE (self->message_entry));
700 [ # # # # : 0 : if (self->attachments == NULL && (text == NULL || *text == '\0'))
# # ]
701 : 0 : return;
702 : :
703 : 0 : builder = g_strv_builder_new ();
704 : 0 : g_hash_table_iter_init (&iter, self->participants);
705 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&recipient, NULL))
706 : 0 : g_strv_builder_add (builder, recipient);
707 : 0 : recipients = g_strv_builder_end (builder);
708 : :
709 : : // FIXME: infer from last message?
710 : 0 : subscription_id = -1;
711 : :
712 : 0 : message = g_object_new (VALENT_TYPE_MESSAGE,
713 : : "iri", NULL,
714 : : "attachments", self->attachments,
715 : : "box", VALENT_MESSAGE_BOX_OUTBOX,
716 : : "date", valent_timestamp_ms (),
717 : : "recipients", recipients,
718 : : "subscription-id", subscription_id,
719 : : "text", text,
720 : : NULL);
721 : :
722 [ # # ]: 0 : valent_messages_adapter_send_message (self->messages,
723 : : message,
724 : : NULL,
725 : : (GAsyncReadyCallback)valent_conversation_page_send_message_cb,
726 : : g_object_ref (self));
727 : : }
728 : :
729 : : /*
730 : : * Details Dialog
731 : : */
732 : : static ValentMessageAttachment *
733 : 0 : valent_message_attachment_from_sparql_cursor (TrackerSparqlCursor *cursor,
734 : : GError **error)
735 : : {
736 : 0 : const char *iri = NULL;
737 : 0 : g_autoptr (GIcon) preview = NULL;
738 [ # # ]: 0 : g_autoptr (GFile) file = NULL;
739 : :
740 [ # # ]: 0 : g_assert (TRACKER_IS_SPARQL_CURSOR (cursor));
741 [ # # # # ]: 0 : g_assert (error == NULL || *error == NULL);
742 : :
743 : 0 : iri = tracker_sparql_cursor_get_string (cursor, 0, NULL);
744 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, 1))
745 : : {
746 : 0 : const char *base64_data;
747 : :
748 : 0 : base64_data = tracker_sparql_cursor_get_string (cursor, 1, NULL);
749 [ # # ]: 0 : if (base64_data != NULL)
750 : : {
751 : 0 : g_autoptr (GBytes) bytes = NULL;
752 : 0 : unsigned char *data;
753 : 0 : size_t len;
754 : :
755 : 0 : data = g_base64_decode (base64_data, &len);
756 : 0 : bytes = g_bytes_new_take (g_steal_pointer (&data), len);
757 [ # # ]: 0 : preview = g_bytes_icon_new (bytes);
758 : : }
759 : : }
760 : :
761 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, 2))
762 : : {
763 : 0 : const char *file_uri;
764 : :
765 : 0 : file_uri = tracker_sparql_cursor_get_string (cursor, 2, NULL);
766 [ # # ]: 0 : if (file_uri != NULL)
767 : 0 : file = g_file_new_for_uri (file_uri);
768 : : }
769 : :
770 [ # # ]: 0 : return g_object_new (VALENT_TYPE_MESSAGE_ATTACHMENT,
771 : : "iri", iri,
772 : : "preview", preview,
773 : : "file", file,
774 : : NULL);
775 : : }
776 : :
777 : : static void
778 : 0 : cursor_get_thread_attachments_cb (TrackerSparqlCursor *cursor,
779 : : GAsyncResult *result,
780 : : gpointer user_data)
781 : : {
782 : 0 : g_autoptr (GListStore) attachments = G_LIST_STORE (g_steal_pointer (&user_data));
783 [ # # ]: 0 : g_autoptr (GError) error = NULL;
784 : :
785 [ # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error))
786 : : {
787 : 0 : ValentMessageAttachment *attachment = NULL;
788 : 0 : GCancellable *cancellable = NULL;
789 : :
790 : 0 : attachment = valent_message_attachment_from_sparql_cursor (cursor, &error);
791 : 0 : g_list_store_append (attachments, attachment);
792 : :
793 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
794 : 0 : tracker_sparql_cursor_next_async (cursor,
795 : : cancellable,
796 : : (GAsyncReadyCallback) cursor_get_thread_attachments_cb,
797 : : g_object_ref (attachments));
798 : : }
799 : : else
800 : : {
801 [ # # ]: 0 : if (error != NULL)
802 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
803 : :
804 : 0 : tracker_sparql_cursor_close (cursor);
805 : : }
806 : 0 : }
807 : :
808 : : static void
809 : 0 : execute_get_thread_attachments_cb (TrackerSparqlStatement *stmt,
810 : : GAsyncResult *result,
811 : : gpointer user_data)
812 : : {
813 : 0 : g_autoptr (GListStore) summary = G_LIST_STORE (g_steal_pointer (&user_data));
814 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
815 : 0 : GCancellable *cancellable = NULL;
816 [ # # ]: 0 : g_autoptr (GError) error = NULL;
817 : :
818 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
819 [ # # ]: 0 : if (cursor == NULL)
820 : : {
821 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
822 [ # # ]: 0 : return;
823 : : }
824 : :
825 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
826 [ # # ]: 0 : tracker_sparql_cursor_next_async (cursor,
827 : : cancellable,
828 : : (GAsyncReadyCallback) cursor_get_thread_attachments_cb,
829 : : g_object_ref (summary));
830 : : }
831 : :
832 : : /*< private >
833 : : * valent_conversation_page_get_attachments:
834 : : * @self: a `ValentConversationPage`
835 : : *
836 : : * Get a list of the attachment for the thread as a `GListModel`.
837 : : *
838 : : * Returns: (transfer full) (nullable): a `GListModel`
839 : : */
840 : : GListModel *
841 : 0 : valent_conversation_page_get_attachments (ValentConversationPage *self)
842 : : {
843 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
844 [ # # ]: 0 : g_autoptr (GListStore) attachments = NULL;
845 [ # # ]: 0 : g_autoptr (GCancellable) cancellable = NULL;
846 : 0 : GError *error = NULL;
847 : :
848 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_CONVERSATION_PAGE (self), NULL);
849 : :
850 : 0 : g_object_get (self->messages, "connection", &connection, NULL);
851 [ # # ]: 0 : if (self->get_thread_attachments_stmt == NULL)
852 : : {
853 : 0 : self->get_thread_attachments_stmt =
854 : 0 : tracker_sparql_connection_load_statement_from_gresource (connection,
855 : : GET_THREAD_ATTACHMENTS_RQ,
856 : : cancellable,
857 : : &error);
858 : : }
859 : :
860 [ # # ]: 0 : if (self->get_thread_attachments_stmt == NULL)
861 : : {
862 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
863 : 0 : return NULL;
864 : : }
865 : :
866 : 0 : attachments = g_list_store_new (VALENT_TYPE_MESSAGE_ATTACHMENT);
867 : 0 : tracker_sparql_statement_bind_string (self->get_thread_attachments_stmt,
868 : : "iri",
869 : 0 : self->iri);
870 : 0 : tracker_sparql_statement_execute_async (self->get_thread_attachments_stmt,
871 : : cancellable,
872 : : (GAsyncReadyCallback) execute_get_thread_attachments_cb,
873 : : g_object_ref (attachments));
874 : :
875 : 0 : return G_LIST_MODEL (g_steal_pointer (&attachments));
876 : : }
877 : :
878 : : static void
879 : 0 : on_contact_selected (ValentContactPage *page,
880 : : EContact *contact,
881 : : const char *target,
882 : : ValentConversationPage *self)
883 : : {
884 : 0 : valent_conversation_page_add_participant (self, contact, target);
885 : 0 : adw_navigation_view_pop (self->details_view);
886 : 0 : }
887 : :
888 : : static void
889 : 0 : on_add_participant (GtkButton *row,
890 : : ValentConversationPage *self)
891 : : {
892 : 0 : AdwNavigationPage *page;
893 : :
894 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
895 : :
896 : 0 : page = g_object_new (VALENT_TYPE_CONTACT_PAGE,
897 : : "tag", "contacts",
898 : : "contacts", self->contacts,
899 : : NULL);
900 : 0 : g_signal_connect_object (page,
901 : : "selected",
902 : : G_CALLBACK (on_contact_selected),
903 : : self,
904 : : G_CONNECT_DEFAULT);
905 : 0 : adw_navigation_view_push (self->details_view, page);
906 : 0 : }
907 : :
908 : : static void
909 : 0 : save_attachment_cb (GtkFileDialog *dialog,
910 : : GAsyncResult *result,
911 : : gpointer user_data)
912 : : {
913 : 0 : g_autoptr (GFile) source = G_FILE (g_steal_pointer (&user_data));
914 [ # # # # ]: 0 : g_autoptr (GFile) target = NULL;
915 [ # # ]: 0 : g_autoptr (GError) error = NULL;
916 : :
917 : 0 : target = gtk_file_dialog_save_finish (dialog, result, &error);
918 [ # # ]: 0 : if (target == NULL)
919 : : {
920 [ # # # # ]: 0 : if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED) &&
921 : 0 : !g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
922 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
923 : :
924 [ # # ]: 0 : return;
925 : : }
926 : :
927 [ # # ]: 0 : g_file_copy_async (source,
928 : : target,
929 : : G_FILE_COPY_NONE,
930 : : G_PRIORITY_DEFAULT,
931 : : NULL, /* cancellable */
932 : : NULL, NULL, /* progress */
933 : : NULL, NULL /* task */);
934 : : }
935 : :
936 : : static void
937 : 0 : on_save_attachment (GtkButton *button,
938 : : GFile *file)
939 : : {
940 : 0 : GtkWidget *widget = GTK_WIDGET (button);
941 : 0 : g_autoptr (GCancellable) cancellable = NULL;
942 : 0 : GtkFileDialog *dialog;
943 : :
944 [ # # # # : 0 : g_assert (G_IS_FILE (file));
# # # # ]
945 : :
946 : 0 : dialog = g_object_new (GTK_TYPE_FILE_DIALOG,
947 : : "title", _("Attach Files"),
948 : : "accept-label", _("Open"),
949 : : NULL);
950 : :
951 : 0 : cancellable = g_cancellable_new ();
952 : 0 : g_signal_connect_object (widget,
953 : : "destroy",
954 : : G_CALLBACK (g_cancellable_cancel),
955 : : cancellable,
956 : : G_CONNECT_SWAPPED);
957 : :
958 [ # # ]: 0 : gtk_file_dialog_save (dialog,
959 : 0 : GTK_WINDOW (gtk_widget_get_root (widget)),
960 : : cancellable,
961 : : (GAsyncReadyCallback) save_attachment_cb,
962 : : g_object_ref (file));
963 : 0 : }
964 : :
965 : :
966 : : static GtkWidget *
967 : 0 : attachment_list_create (gpointer item,
968 : : gpointer user_data)
969 : : {
970 : 0 : ValentMessageAttachment *attachment = VALENT_MESSAGE_ATTACHMENT (item);
971 : 0 : GtkWidget *row;
972 : 0 : GtkWidget *image;
973 : 0 : GtkWidget *button;
974 : 0 : GIcon *preview;
975 : 0 : GFile *file;
976 : 0 : g_autofree char *filename = NULL;
977 : :
978 : 0 : preview = valent_message_attachment_get_preview (attachment);
979 : 0 : file = valent_message_attachment_get_file (attachment);
980 [ # # ]: 0 : if (file != NULL)
981 : 0 : filename = g_file_get_basename (file);
982 : :
983 : 0 : row = g_object_new (ADW_TYPE_ACTION_ROW,
984 : : "title", filename,
985 : : "title-lines", 1,
986 : : NULL);
987 : :
988 : 0 : image = g_object_new (GTK_TYPE_IMAGE,
989 : : "gicon", preview,
990 : : "pixel-size", 48,
991 : : "overflow", GTK_OVERFLOW_HIDDEN,
992 : : "tooltip-text", filename,
993 : : "halign", GTK_ALIGN_START,
994 : : NULL);
995 : 0 : adw_action_row_add_prefix (ADW_ACTION_ROW (row), image);
996 : :
997 [ # # ]: 0 : if (file != NULL)
998 : : {
999 : 0 : button = g_object_new (GTK_TYPE_BUTTON,
1000 : : "icon-name", "document-save-symbolic",
1001 : : "tooltip-text", _("Save"),
1002 : : "valign", GTK_ALIGN_CENTER,
1003 : : NULL);
1004 : 0 : gtk_widget_add_css_class (GTK_WIDGET (button), "circular");
1005 : 0 : gtk_widget_add_css_class (GTK_WIDGET (button), "flat");
1006 : 0 : adw_action_row_add_suffix (ADW_ACTION_ROW (row), button);
1007 : 0 : g_signal_connect_object (button,
1008 : : "clicked",
1009 : : G_CALLBACK (on_save_attachment),
1010 : : file,
1011 : : G_CONNECT_DEFAULT);
1012 : : }
1013 : :
1014 : 0 : return row;
1015 : : }
1016 : :
1017 : : static void
1018 : 0 : conversation_details_action (GtkWidget *widget,
1019 : : const char *action_name,
1020 : : GVariant *parameters)
1021 : : {
1022 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1023 : 0 : g_autoptr (GListModel) attachments = NULL;
1024 : :
1025 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1026 : :
1027 : 0 : attachments = valent_conversation_page_get_attachments (self);
1028 : 0 : gtk_list_box_bind_model (GTK_LIST_BOX (self->attachment_list),
1029 : : attachments,
1030 : : attachment_list_create,
1031 : : NULL,
1032 : : NULL);
1033 : :
1034 [ # # ]: 0 : adw_dialog_present (self->details_dialog, widget);
1035 : 0 : }
1036 : :
1037 : : static void
1038 : 0 : gtk_file_dialog_open_multiple_cb (GtkFileDialog *dialog,
1039 : : GAsyncResult *result,
1040 : : ValentConversationPage *self)
1041 : : {
1042 : 0 : g_autoptr (GListModel) files = NULL;
1043 : 0 : unsigned int n_files;
1044 : 0 : g_autoptr (GError) error = NULL;
1045 : :
1046 : 0 : files = gtk_file_dialog_open_multiple_finish (dialog, result, &error);
1047 [ # # ]: 0 : if (files == NULL)
1048 : : {
1049 [ # # # # ]: 0 : if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED) &&
1050 : 0 : !g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
1051 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
1052 : :
1053 [ # # ]: 0 : return;
1054 : : }
1055 : :
1056 [ # # ]: 0 : if (self->attachments == NULL)
1057 : 0 : self->attachments = g_list_store_new (VALENT_TYPE_MESSAGE_ATTACHMENT);
1058 : :
1059 : 0 : n_files = g_list_model_get_n_items (files);
1060 [ # # ]: 0 : for (unsigned int i = 0; i < n_files; i++)
1061 : : {
1062 : 0 : g_autoptr (ValentMessageAttachment) attachment = NULL;
1063 [ # # ]: 0 : g_autoptr (GFile) file = NULL;
1064 : :
1065 : 0 : file = g_list_model_get_item (files, i);
1066 : 0 : attachment = g_object_new (VALENT_TYPE_MESSAGE_ATTACHMENT,
1067 : : "file", file,
1068 : : NULL);
1069 [ # # ]: 0 : g_list_store_append (self->attachments, attachment);
1070 : : }
1071 : :
1072 [ # # ]: 0 : valent_conversation_page_check_message (self);
1073 : : }
1074 : :
1075 : : /**
1076 : : * ValentSharePlugin|message.attachment:
1077 : : * @parameter: %NULL
1078 : : *
1079 : : * The default share action opens the platform-specific dialog for selecting
1080 : : * files, typically a `GtkFileChooserDialog`.
1081 : : */
1082 : : static void
1083 : 0 : message_attachment_action (GtkWidget *widget,
1084 : : const char *action_name,
1085 : : GVariant *parameters)
1086 : : {
1087 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1088 : 0 : g_autoptr (GCancellable) cancellable = NULL;
1089 : 0 : GtkFileDialog *dialog;
1090 : :
1091 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1092 : :
1093 : 0 : dialog = g_object_new (GTK_TYPE_FILE_DIALOG,
1094 : : "title", _("Attach Files"),
1095 : : "accept-label", _("Open"),
1096 : : NULL);
1097 : :
1098 : 0 : cancellable = g_cancellable_new ();
1099 : 0 : g_signal_connect_object (widget,
1100 : : "destroy",
1101 : : G_CALLBACK (g_cancellable_cancel),
1102 : : cancellable,
1103 : : G_CONNECT_SWAPPED);
1104 : :
1105 [ # # ]: 0 : gtk_file_dialog_open_multiple (dialog,
1106 : 0 : GTK_WINDOW (gtk_widget_get_root (widget)),
1107 : : cancellable,
1108 : : (GAsyncReadyCallback) gtk_file_dialog_open_multiple_cb,
1109 : : self);
1110 : 0 : }
1111 : :
1112 : : static void
1113 : 0 : message_send_action (GtkWidget *widget,
1114 : : const char *action_name,
1115 : : GVariant *parameters)
1116 : : {
1117 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1118 : :
1119 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1120 : :
1121 : 0 : valent_conversation_page_send_message (self);
1122 : 0 : }
1123 : :
1124 : : /*
1125 : : * ValentConversationPage
1126 : : */
1127 : : static void
1128 : 1 : valent_conversation_page_set_iri (ValentConversationPage *self,
1129 : : const char *iri)
1130 : : {
1131 [ + - ]: 1 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1132 [ - + - - ]: 1 : g_assert (iri == NULL || *iri != '\0');
1133 : :
1134 [ - + ]: 1 : if (g_set_str (&self->iri, iri))
1135 : 0 : valent_conversation_page_load (self);
1136 : 1 : }
1137 : :
1138 : : /*
1139 : : * AdwNavigationPage
1140 : : */
1141 : : static void
1142 : 0 : valent_conversation_page_shown (AdwNavigationPage *page)
1143 : : {
1144 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (page);
1145 : :
1146 : 0 : gtk_widget_action_set_enabled (GTK_WIDGET (self), "message.send", FALSE);
1147 : 0 : gtk_widget_grab_focus (GTK_WIDGET (self->message_entry));
1148 : :
1149 [ # # ]: 0 : if (ADW_NAVIGATION_PAGE_CLASS (valent_conversation_page_parent_class)->shown)
1150 : 0 : ADW_NAVIGATION_PAGE_CLASS (valent_conversation_page_parent_class)->shown (page);
1151 : 0 : }
1152 : :
1153 : : /*
1154 : : * GObject
1155 : : */
1156 : : static void
1157 : 1 : valent_conversation_page_dispose (GObject *object)
1158 : : {
1159 : 1 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1160 : :
1161 [ - + ]: 1 : if (self->thread != NULL)
1162 : : {
1163 : 0 : g_signal_handlers_disconnect_by_data (self->thread, self);
1164 [ # # ]: 0 : g_clear_object (&self->thread);
1165 : : }
1166 : :
1167 : 1 : gtk_widget_dispose_template (GTK_WIDGET (object),
1168 : : VALENT_TYPE_CONVERSATION_PAGE);
1169 : :
1170 : 1 : G_OBJECT_CLASS (valent_conversation_page_parent_class)->dispose (object);
1171 : 1 : }
1172 : :
1173 : : static void
1174 : 1 : valent_conversation_page_finalize (GObject *object)
1175 : : {
1176 : 1 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1177 : :
1178 [ - + ]: 1 : g_clear_object (&self->messages);
1179 [ - + ]: 1 : g_clear_object (&self->contacts);
1180 [ - + ]: 1 : g_clear_object (&self->thread);
1181 [ - + ]: 1 : g_clear_pointer (&self->iri, g_free);
1182 [ + - ]: 1 : g_clear_pointer (&self->participants, g_hash_table_unref);
1183 [ + - ]: 1 : g_clear_pointer (&self->outbox, g_hash_table_unref);
1184 [ - + ]: 1 : g_clear_object (&self->attachments);
1185 : :
1186 : 1 : G_OBJECT_CLASS (valent_conversation_page_parent_class)->finalize (object);
1187 : 1 : }
1188 : :
1189 : : static void
1190 : 3 : valent_conversation_page_get_property (GObject *object,
1191 : : guint prop_id,
1192 : : GValue *value,
1193 : : GParamSpec *pspec)
1194 : : {
1195 : 3 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1196 : :
1197 [ + + + - ]: 3 : switch ((ValentConversationPageProperty)prop_id)
1198 : : {
1199 : 1 : case PROP_CONTACTS:
1200 : 1 : g_value_set_object (value, self->contacts);
1201 : 1 : break;
1202 : :
1203 : 1 : case PROP_MESSAGES:
1204 : 1 : g_value_set_object (value, self->messages);
1205 : 1 : break;
1206 : :
1207 : 1 : case PROP_IRI:
1208 : 1 : g_value_set_string (value, self->iri);
1209 : 1 : break;
1210 : :
1211 : 0 : default:
1212 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1213 : : }
1214 : 3 : }
1215 : :
1216 : : static void
1217 : 3 : valent_conversation_page_set_property (GObject *object,
1218 : : guint prop_id,
1219 : : const GValue *value,
1220 : : GParamSpec *pspec)
1221 : : {
1222 : 3 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1223 : :
1224 [ + + + - ]: 3 : switch ((ValentConversationPageProperty)prop_id)
1225 : : {
1226 : 1 : case PROP_CONTACTS:
1227 : 1 : g_set_object (&self->contacts, g_value_get_object (value));
1228 : 1 : break;
1229 : :
1230 : 1 : case PROP_MESSAGES:
1231 : 1 : g_set_object (&self->messages, g_value_get_object (value));
1232 : 1 : break;
1233 : :
1234 : 1 : case PROP_IRI:
1235 : 1 : valent_conversation_page_set_iri (self, g_value_get_string (value));
1236 : 1 : break;
1237 : :
1238 : 0 : default:
1239 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1240 : : }
1241 : 3 : }
1242 : :
1243 : : static void
1244 : 1 : valent_conversation_page_class_init (ValentConversationPageClass *klass)
1245 : : {
1246 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
1247 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1248 : 1 : AdwNavigationPageClass *page_class = ADW_NAVIGATION_PAGE_CLASS (klass);
1249 : :
1250 : 1 : object_class->dispose = valent_conversation_page_dispose;
1251 : 1 : object_class->finalize = valent_conversation_page_finalize;
1252 : 1 : object_class->get_property = valent_conversation_page_get_property;
1253 : 1 : object_class->set_property = valent_conversation_page_set_property;
1254 : :
1255 : 1 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/gnome/valent-conversation-page.ui");
1256 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, message_list);
1257 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, message_entry);
1258 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, scrolledwindow);
1259 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, vadjustment);
1260 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, details_dialog);
1261 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, details_view);
1262 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, participant_list);
1263 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, attachment_list);
1264 : :
1265 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_scroll_upper_changed);
1266 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_scroll_value_changed);
1267 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_entry_activated);
1268 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_entry_changed);
1269 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_add_participant);
1270 : 1 : gtk_widget_class_install_action (widget_class, "conversation.details", NULL, conversation_details_action);
1271 : 1 : gtk_widget_class_install_action (widget_class, "message.attachment", NULL, message_attachment_action);
1272 : 1 : gtk_widget_class_install_action (widget_class, "message.send", NULL, message_send_action);
1273 : :
1274 : 1 : page_class->shown = valent_conversation_page_shown;
1275 : :
1276 : 2 : properties [PROP_CONTACTS] =
1277 : 1 : g_param_spec_object ("contacts", NULL, NULL,
1278 : : VALENT_TYPE_CONTACTS_ADAPTER,
1279 : : (G_PARAM_READWRITE |
1280 : : G_PARAM_CONSTRUCT |
1281 : : G_PARAM_STATIC_STRINGS));
1282 : :
1283 : 2 : properties [PROP_MESSAGES] =
1284 : 1 : g_param_spec_object ("messages", NULL, NULL,
1285 : : VALENT_TYPE_MESSAGES_ADAPTER,
1286 : : (G_PARAM_READWRITE |
1287 : : G_PARAM_CONSTRUCT_ONLY |
1288 : : G_PARAM_STATIC_STRINGS));
1289 : :
1290 : 2 : properties [PROP_IRI] =
1291 : 1 : g_param_spec_string ("iri", NULL, NULL,
1292 : : NULL,
1293 : : (G_PARAM_READWRITE |
1294 : : G_PARAM_CONSTRUCT_ONLY |
1295 : : G_PARAM_STATIC_STRINGS));
1296 : :
1297 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
1298 : 1 : }
1299 : :
1300 : : static uint32_t
1301 : 0 : contact_medium_hash (gconstpointer medium)
1302 : : {
1303 : 0 : g_autoptr (EPhoneNumber) number = NULL;
1304 [ # # ]: 0 : g_autofree char *number_str = NULL;
1305 : :
1306 [ # # ]: 0 : if G_UNLIKELY (g_strrstr (medium, "@"))
1307 : 0 : return g_str_hash (medium);
1308 : :
1309 : 0 : number = e_phone_number_from_string (medium, NULL, NULL);
1310 : 0 : number_str = e_phone_number_to_string (number, E_PHONE_NUMBER_FORMAT_E164);
1311 : :
1312 : 0 : return g_str_hash (number_str);
1313 : : }
1314 : :
1315 : : static gboolean
1316 : 0 : contact_medium_equal (gconstpointer medium1,
1317 : : gconstpointer medium2)
1318 : : {
1319 [ # # # # ]: 0 : if G_UNLIKELY (g_strrstr (medium1, "@") || g_strrstr (medium1, "@"))
1320 : 0 : return g_str_equal (medium1, medium2);
1321 : :
1322 : 0 : return e_phone_number_compare_strings (medium1, medium2, NULL) != E_PHONE_NUMBER_MATCH_NONE;
1323 : : }
1324 : :
1325 : : static void
1326 : 1 : valent_conversation_page_init (ValentConversationPage *self)
1327 : : {
1328 : 1 : gtk_widget_init_template (GTK_WIDGET (self));
1329 : :
1330 : 1 : gtk_list_box_set_header_func (self->message_list,
1331 : : message_list_header_func,
1332 : : self, NULL);
1333 : :
1334 : 1 : self->participants = g_hash_table_new_full (contact_medium_hash,
1335 : : contact_medium_equal,
1336 : : g_free,
1337 : : g_object_unref);
1338 : 1 : self->outbox = g_hash_table_new_full (NULL,
1339 : : NULL,
1340 : : g_object_unref,
1341 : : g_object_unref);
1342 : 1 : }
1343 : :
1344 : : /**
1345 : : * valent_conversation_page_get_iri:
1346 : : * @conversation: a `ValentConversationPage`
1347 : : *
1348 : : * Get the thread IRI for @conversation.
1349 : : *
1350 : : * Returns: the thread IRI
1351 : : */
1352 : : const char *
1353 : 1 : valent_conversation_page_get_iri (ValentConversationPage *conversation)
1354 : : {
1355 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_CONVERSATION_PAGE (conversation), NULL);
1356 : :
1357 : 1 : return conversation->iri;
1358 : : }
1359 : :
1360 : : /**
1361 : : * valent_conversation_page_add_participant:
1362 : : * @conversation: a `ValentConversationPage`
1363 : : * @contact: an `EContact`
1364 : : * @medium: a contact IRI
1365 : : *
1366 : : * Add @contact to @conversation, with the contact point @medium.
1367 : : */
1368 : : void
1369 : 0 : valent_conversation_page_add_participant (ValentConversationPage *conversation,
1370 : : EContact *contact,
1371 : : const char *medium)
1372 : : {
1373 : 0 : GHashTableIter iter;
1374 : 0 : GtkWidget *child;
1375 : 0 : size_t position = 0;
1376 : 0 : gboolean is_new = FALSE;
1377 : :
1378 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (conversation));
1379 [ # # # # : 0 : g_return_if_fail (E_IS_CONTACT (contact));
# # # # ]
1380 [ # # # # ]: 0 : g_return_if_fail (medium != NULL && *medium != '\0');
1381 : :
1382 : : // FIXME: use vmo:hasParticipant
1383 [ # # ]: 0 : is_new = g_hash_table_replace (conversation->participants,
1384 : 0 : g_strdup (medium),
1385 : : g_object_ref (contact));
1386 [ # # ]: 0 : if (!is_new)
1387 : : return;
1388 : :
1389 : : /* Clear the dialog
1390 : : */
1391 : 0 : child = gtk_widget_get_first_child (conversation->participant_list);
1392 [ # # ]: 0 : while (child != NULL)
1393 : : {
1394 : 0 : gtk_list_box_remove (GTK_LIST_BOX (conversation->participant_list), child);
1395 : 0 : child = gtk_widget_get_first_child (conversation->participant_list);
1396 : : }
1397 : :
1398 : : /* Update the dialog
1399 : : */
1400 : 0 : g_hash_table_iter_init (&iter, conversation->participants);
1401 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&medium, (void **)&contact))
1402 : : {
1403 : 0 : const char *name = NULL;
1404 : :
1405 : 0 : name = e_contact_get_const (contact, E_CONTACT_FULL_NAME);
1406 : 0 : adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (conversation), name);
1407 : :
1408 : 0 : child = g_object_new (VALENT_TYPE_CONTACT_ROW,
1409 : : "contact", contact,
1410 : : "contact-medium", medium,
1411 : : NULL);
1412 : 0 : gtk_list_box_insert (GTK_LIST_BOX (conversation->participant_list),
1413 : : child,
1414 : 0 : position++);
1415 : : }
1416 : : }
1417 : :
1418 : : static void
1419 : 0 : valent_conversation_page_scroll_to_row (ValentConversationPage *self,
1420 : : GtkWidget *row)
1421 : : {
1422 : 0 : GtkWidget *viewport;
1423 : 0 : double upper, page_size;
1424 : 0 : double target, maximum;
1425 : :
1426 : 0 : upper = gtk_adjustment_get_upper (self->vadjustment);
1427 : 0 : page_size = gtk_adjustment_get_page_size (self->vadjustment);
1428 : 0 : maximum = upper - page_size;
1429 : 0 : target = upper - page_size;
1430 : :
1431 [ # # ]: 0 : if (row != NULL)
1432 : : {
1433 : 0 : graphene_rect_t row_bounds;
1434 : 0 : graphene_point_t row_point;
1435 : 0 : graphene_point_t target_point;
1436 : :
1437 : 0 : viewport = gtk_scrolled_window_get_child (self->scrolledwindow);
1438 [ # # ]: 0 : if (!gtk_widget_compute_bounds (row, viewport, &row_bounds))
1439 : : {
1440 : 0 : g_warning ("%s(): failed to scroll to row", G_STRFUNC);
1441 : 0 : return;
1442 : : }
1443 : :
1444 : 0 : graphene_rect_get_bottom_right (&row_bounds, &row_point);
1445 [ # # ]: 0 : if (!gtk_widget_compute_point (row, viewport, &row_point, &target_point))
1446 : : {
1447 : 0 : g_warning ("%s(): failed to scroll to row", G_STRFUNC);
1448 : 0 : return;
1449 : : }
1450 : :
1451 : 0 : target = target_point.y;
1452 : : }
1453 : :
1454 : 0 : gtk_scrolled_window_set_kinetic_scrolling (self->scrolledwindow, FALSE);
1455 [ # # ]: 0 : gtk_adjustment_set_value (self->vadjustment, CLAMP (target, 0, maximum));
1456 : 0 : gtk_scrolled_window_set_kinetic_scrolling (self->scrolledwindow, TRUE);
1457 : : }
1458 : :
1459 : : /**
1460 : : * valent_conversation_page_scroll_to_date:
1461 : : * @page: a `ValentConversationPage`
1462 : : * @date: a UNIX epoch timestamp
1463 : : *
1464 : : * Scroll to the message closest to @date.
1465 : : */
1466 : : void
1467 : 0 : valent_conversation_page_scroll_to_date (ValentConversationPage *page,
1468 : : int64_t date)
1469 : : {
1470 : 0 : GtkWidget *row;
1471 : 0 : ValentMessage *message;
1472 : :
1473 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (page));
1474 [ # # ]: 0 : g_return_if_fail (date > 0);
1475 : :
1476 : : /* First look through the list box */
1477 : 0 : for (row = gtk_widget_get_last_child (GTK_WIDGET (page->message_list));
1478 [ # # ]: 0 : row != NULL;
1479 : 0 : row = gtk_widget_get_prev_sibling (row))
1480 : : {
1481 : : /* If this message is equal or older than the target date, we're done
1482 : : */
1483 [ # # ]: 0 : if (valent_conversation_row_get_date (VALENT_CONVERSATION_ROW (row)) <= date)
1484 : : {
1485 : 0 : valent_conversation_page_scroll_to_row (page, row);
1486 : 0 : return;
1487 : : }
1488 : : }
1489 : :
1490 : : /* If there are no more messages, we're done
1491 : : */
1492 [ # # ]: 0 : g_return_if_fail (G_IS_LIST_MODEL (page->thread));
1493 : :
1494 : : /* Populate the list in reverse until we find the message
1495 : : */
1496 [ # # ]: 0 : while ((message = valent_conversation_page_pop_tail (page)) != NULL)
1497 : : {
1498 : : /* Prepend the message
1499 : : */
1500 : 0 : row = valent_conversation_page_insert_message (page, message, 0);
1501 : 0 : g_object_unref (message);
1502 : :
1503 : : /* If this message is equal or older than the target date, we're done
1504 : : */
1505 [ # # ]: 0 : if (valent_message_get_date (message) <= date)
1506 : : {
1507 : 0 : valent_conversation_page_scroll_to_row (page, row);
1508 : 0 : return;
1509 : : }
1510 : : }
1511 : : }
1512 : :
1513 : : /**
1514 : : * valent_conversation_page_scroll_to_message:
1515 : : * @page: a `ValentConversationPage`
1516 : : * @message: a `ValentMessage`
1517 : : *
1518 : : * A convenience for calling valent_message_get_date() and then
1519 : : * valent_conversation_page_scroll_to_date().
1520 : : */
1521 : : void
1522 : 0 : valent_conversation_page_scroll_to_message (ValentConversationPage *page,
1523 : : ValentMessage *message)
1524 : : {
1525 : 0 : int64_t date;
1526 : :
1527 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (page));
1528 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGE (message));
1529 : :
1530 : 0 : date = valent_message_get_date (message);
1531 : 0 : valent_conversation_page_scroll_to_date (page, date);
1532 : : }
1533 : :
|