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 : 0 : gtk_widget_set_sensitive (self->message_entry, TRUE);
674 : : }
675 [ # # ]: 0 : else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
676 : : {
677 : 0 : gtk_widget_add_css_class (self->message_entry, "error");
678 : 0 : gtk_widget_set_sensitive (self->message_entry, TRUE);
679 : : }
680 : 0 : }
681 : :
682 : : /*< private >
683 : : * valent_conversation_page_send_message:
684 : : * @self: a `ValentConversationPage`
685 : : *
686 : : * Send the current text and/or attachment provided by the user.
687 : : */
688 : : static void
689 : 0 : valent_conversation_page_send_message (ValentConversationPage *self)
690 : : {
691 : 0 : g_autoptr (ValentMessage) message = NULL;
692 [ # # ]: 0 : g_autoptr (GStrvBuilder) builder = NULL;
693 [ # # ]: 0 : g_auto (GStrv) recipients = NULL;
694 : 0 : GHashTableIter iter;
695 : 0 : const char *recipient;
696 : 0 : int64_t subscription_id;
697 : 0 : const char *text;
698 : :
699 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
700 : :
701 : 0 : text = gtk_editable_get_text (GTK_EDITABLE (self->message_entry));
702 [ # # # # : 0 : if (self->attachments == NULL && (text == NULL || *text == '\0'))
# # ]
703 : 0 : return;
704 : :
705 : 0 : builder = g_strv_builder_new ();
706 : 0 : g_hash_table_iter_init (&iter, self->participants);
707 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&recipient, NULL))
708 : 0 : g_strv_builder_add (builder, recipient);
709 : 0 : recipients = g_strv_builder_end (builder);
710 : :
711 : : // FIXME: infer from last message?
712 : 0 : subscription_id = -1;
713 : :
714 : 0 : message = g_object_new (VALENT_TYPE_MESSAGE,
715 : : "iri", NULL,
716 : : "attachments", self->attachments,
717 : : "box", VALENT_MESSAGE_BOX_OUTBOX,
718 : : "date", valent_timestamp_ms (),
719 : : "recipients", recipients,
720 : : "subscription-id", subscription_id,
721 : : "text", text,
722 : : NULL);
723 : :
724 : 0 : valent_messages_adapter_send_message (self->messages,
725 : : message,
726 : : NULL,
727 : : (GAsyncReadyCallback)valent_conversation_page_send_message_cb,
728 : : g_object_ref (self));
729 [ # # ]: 0 : gtk_widget_set_sensitive (self->message_entry, FALSE);
730 : : }
731 : :
732 : : /*
733 : : * Details Dialog
734 : : */
735 : : static ValentMessageAttachment *
736 : 0 : valent_message_attachment_from_sparql_cursor (TrackerSparqlCursor *cursor,
737 : : GError **error)
738 : : {
739 : 0 : const char *iri = NULL;
740 : 0 : g_autoptr (GIcon) preview = NULL;
741 [ # # ]: 0 : g_autoptr (GFile) file = NULL;
742 : :
743 [ # # ]: 0 : g_assert (TRACKER_IS_SPARQL_CURSOR (cursor));
744 [ # # # # ]: 0 : g_assert (error == NULL || *error == NULL);
745 : :
746 : 0 : iri = tracker_sparql_cursor_get_string (cursor, 0, NULL);
747 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, 1))
748 : : {
749 : 0 : const char *base64_data;
750 : :
751 : 0 : base64_data = tracker_sparql_cursor_get_string (cursor, 1, NULL);
752 [ # # ]: 0 : if (base64_data != NULL)
753 : : {
754 : 0 : g_autoptr (GBytes) bytes = NULL;
755 : 0 : unsigned char *data;
756 : 0 : size_t len;
757 : :
758 : 0 : data = g_base64_decode (base64_data, &len);
759 : 0 : bytes = g_bytes_new_take (g_steal_pointer (&data), len);
760 [ # # ]: 0 : preview = g_bytes_icon_new (bytes);
761 : : }
762 : : }
763 : :
764 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, 2))
765 : : {
766 : 0 : const char *file_uri;
767 : :
768 : 0 : file_uri = tracker_sparql_cursor_get_string (cursor, 2, NULL);
769 [ # # ]: 0 : if (file_uri != NULL)
770 : 0 : file = g_file_new_for_uri (file_uri);
771 : : }
772 : :
773 [ # # ]: 0 : return g_object_new (VALENT_TYPE_MESSAGE_ATTACHMENT,
774 : : "iri", iri,
775 : : "preview", preview,
776 : : "file", file,
777 : : NULL);
778 : : }
779 : :
780 : : static void
781 : 0 : cursor_get_thread_attachments_cb (TrackerSparqlCursor *cursor,
782 : : GAsyncResult *result,
783 : : gpointer user_data)
784 : : {
785 : 0 : g_autoptr (GListStore) attachments = G_LIST_STORE (g_steal_pointer (&user_data));
786 [ # # ]: 0 : g_autoptr (GError) error = NULL;
787 : :
788 [ # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error))
789 : : {
790 : 0 : ValentMessageAttachment *attachment = NULL;
791 : 0 : GCancellable *cancellable = NULL;
792 : :
793 : 0 : attachment = valent_message_attachment_from_sparql_cursor (cursor, &error);
794 : 0 : g_list_store_append (attachments, attachment);
795 : :
796 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
797 : 0 : tracker_sparql_cursor_next_async (cursor,
798 : : cancellable,
799 : : (GAsyncReadyCallback) cursor_get_thread_attachments_cb,
800 : : g_object_ref (attachments));
801 : : }
802 : : else
803 : : {
804 [ # # ]: 0 : if (error != NULL)
805 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
806 : :
807 : 0 : tracker_sparql_cursor_close (cursor);
808 : : }
809 : 0 : }
810 : :
811 : : static void
812 : 0 : execute_get_thread_attachments_cb (TrackerSparqlStatement *stmt,
813 : : GAsyncResult *result,
814 : : gpointer user_data)
815 : : {
816 : 0 : g_autoptr (GListStore) summary = G_LIST_STORE (g_steal_pointer (&user_data));
817 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
818 : 0 : GCancellable *cancellable = NULL;
819 [ # # ]: 0 : g_autoptr (GError) error = NULL;
820 : :
821 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
822 [ # # ]: 0 : if (cursor == NULL)
823 : : {
824 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
825 [ # # ]: 0 : return;
826 : : }
827 : :
828 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
829 [ # # ]: 0 : tracker_sparql_cursor_next_async (cursor,
830 : : cancellable,
831 : : (GAsyncReadyCallback) cursor_get_thread_attachments_cb,
832 : : g_object_ref (summary));
833 : : }
834 : :
835 : : /*< private >
836 : : * valent_conversation_page_get_attachments:
837 : : * @self: a `ValentConversationPage`
838 : : *
839 : : * Get a list of the attachment for the thread as a `GListModel`.
840 : : *
841 : : * Returns: (transfer full) (nullable): a `GListModel`
842 : : */
843 : : GListModel *
844 : 0 : valent_conversation_page_get_attachments (ValentConversationPage *self)
845 : : {
846 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
847 [ # # ]: 0 : g_autoptr (GListStore) attachments = NULL;
848 [ # # ]: 0 : g_autoptr (GCancellable) cancellable = NULL;
849 : 0 : GError *error = NULL;
850 : :
851 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_CONVERSATION_PAGE (self), NULL);
852 : :
853 : 0 : g_object_get (self->messages, "connection", &connection, NULL);
854 [ # # ]: 0 : if (self->get_thread_attachments_stmt == NULL)
855 : : {
856 : 0 : self->get_thread_attachments_stmt =
857 : 0 : tracker_sparql_connection_load_statement_from_gresource (connection,
858 : : GET_THREAD_ATTACHMENTS_RQ,
859 : : cancellable,
860 : : &error);
861 : : }
862 : :
863 [ # # ]: 0 : if (self->get_thread_attachments_stmt == NULL)
864 : : {
865 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
866 : 0 : return NULL;
867 : : }
868 : :
869 : 0 : attachments = g_list_store_new (VALENT_TYPE_MESSAGE_ATTACHMENT);
870 : 0 : tracker_sparql_statement_bind_string (self->get_thread_attachments_stmt,
871 : : "iri",
872 : 0 : self->iri);
873 : 0 : tracker_sparql_statement_execute_async (self->get_thread_attachments_stmt,
874 : : cancellable,
875 : : (GAsyncReadyCallback) execute_get_thread_attachments_cb,
876 : : g_object_ref (attachments));
877 : :
878 : 0 : return G_LIST_MODEL (g_steal_pointer (&attachments));
879 : : }
880 : :
881 : : static void
882 : 0 : on_contact_selected (ValentContactPage *page,
883 : : EContact *contact,
884 : : const char *target,
885 : : ValentConversationPage *self)
886 : : {
887 : 0 : valent_conversation_page_add_participant (self, contact, target);
888 : 0 : adw_navigation_view_pop (self->details_view);
889 : 0 : }
890 : :
891 : : static void
892 : 0 : on_add_participant (GtkButton *row,
893 : : ValentConversationPage *self)
894 : : {
895 : 0 : AdwNavigationPage *page;
896 : :
897 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
898 : :
899 : 0 : page = g_object_new (VALENT_TYPE_CONTACT_PAGE,
900 : : "tag", "contacts",
901 : : "contacts", self->contacts,
902 : : NULL);
903 : 0 : g_signal_connect_object (page,
904 : : "selected",
905 : : G_CALLBACK (on_contact_selected),
906 : : self,
907 : : G_CONNECT_DEFAULT);
908 : 0 : adw_navigation_view_push (self->details_view, page);
909 : 0 : }
910 : :
911 : : static void
912 : 0 : save_attachment_cb (GtkFileDialog *dialog,
913 : : GAsyncResult *result,
914 : : gpointer user_data)
915 : : {
916 : 0 : g_autoptr (GFile) source = G_FILE (g_steal_pointer (&user_data));
917 [ # # # # ]: 0 : g_autoptr (GFile) target = NULL;
918 [ # # ]: 0 : g_autoptr (GError) error = NULL;
919 : :
920 : 0 : target = gtk_file_dialog_save_finish (dialog, result, &error);
921 [ # # ]: 0 : if (target == NULL)
922 : : {
923 [ # # # # ]: 0 : if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED) &&
924 : 0 : !g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
925 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
926 : :
927 [ # # ]: 0 : return;
928 : : }
929 : :
930 [ # # ]: 0 : g_file_copy_async (source,
931 : : target,
932 : : G_FILE_COPY_NONE,
933 : : G_PRIORITY_DEFAULT,
934 : : NULL, /* cancellable */
935 : : NULL, NULL, /* progress */
936 : : NULL, NULL /* task */);
937 : : }
938 : :
939 : : static void
940 : 0 : on_save_attachment (GtkButton *button,
941 : : GFile *file)
942 : : {
943 : 0 : GtkWidget *widget = GTK_WIDGET (button);
944 : 0 : g_autoptr (GCancellable) cancellable = NULL;
945 : 0 : GtkFileDialog *dialog;
946 : :
947 [ # # # # : 0 : g_assert (G_IS_FILE (file));
# # # # ]
948 : :
949 : 0 : dialog = g_object_new (GTK_TYPE_FILE_DIALOG,
950 : : "title", _("Attach Files"),
951 : : "accept-label", _("Open"),
952 : : NULL);
953 : :
954 : 0 : cancellable = g_cancellable_new ();
955 : 0 : g_signal_connect_object (widget,
956 : : "destroy",
957 : : G_CALLBACK (g_cancellable_cancel),
958 : : cancellable,
959 : : G_CONNECT_SWAPPED);
960 : :
961 [ # # ]: 0 : gtk_file_dialog_save (dialog,
962 : 0 : GTK_WINDOW (gtk_widget_get_root (widget)),
963 : : cancellable,
964 : : (GAsyncReadyCallback) save_attachment_cb,
965 : : g_object_ref (file));
966 : 0 : }
967 : :
968 : :
969 : : static GtkWidget *
970 : 0 : attachment_list_create (gpointer item,
971 : : gpointer user_data)
972 : : {
973 : 0 : ValentMessageAttachment *attachment = VALENT_MESSAGE_ATTACHMENT (item);
974 : 0 : GtkWidget *row;
975 : 0 : GtkWidget *image;
976 : 0 : GtkWidget *button;
977 : 0 : GIcon *preview;
978 : 0 : GFile *file;
979 : 0 : g_autofree char *filename = NULL;
980 : :
981 : 0 : preview = valent_message_attachment_get_preview (attachment);
982 : 0 : file = valent_message_attachment_get_file (attachment);
983 [ # # ]: 0 : if (file != NULL)
984 : 0 : filename = g_file_get_basename (file);
985 : :
986 : 0 : row = g_object_new (ADW_TYPE_ACTION_ROW,
987 : : "title", filename,
988 : : "title-lines", 1,
989 : : NULL);
990 : :
991 : 0 : image = g_object_new (GTK_TYPE_IMAGE,
992 : : "gicon", preview,
993 : : "pixel-size", 48,
994 : : "overflow", GTK_OVERFLOW_HIDDEN,
995 : : "tooltip-text", filename,
996 : : "halign", GTK_ALIGN_START,
997 : : NULL);
998 : 0 : adw_action_row_add_prefix (ADW_ACTION_ROW (row), image);
999 : :
1000 [ # # ]: 0 : if (file != NULL)
1001 : : {
1002 : 0 : button = g_object_new (GTK_TYPE_BUTTON,
1003 : : "icon-name", "document-save-symbolic",
1004 : : "tooltip-text", _("Save"),
1005 : : "valign", GTK_ALIGN_CENTER,
1006 : : NULL);
1007 : 0 : gtk_widget_add_css_class (GTK_WIDGET (button), "circular");
1008 : 0 : gtk_widget_add_css_class (GTK_WIDGET (button), "flat");
1009 : 0 : adw_action_row_add_suffix (ADW_ACTION_ROW (row), button);
1010 : 0 : g_signal_connect_object (button,
1011 : : "clicked",
1012 : : G_CALLBACK (on_save_attachment),
1013 : : file,
1014 : : G_CONNECT_DEFAULT);
1015 : : }
1016 : :
1017 : 0 : return row;
1018 : : }
1019 : :
1020 : : static void
1021 : 0 : conversation_details_action (GtkWidget *widget,
1022 : : const char *action_name,
1023 : : GVariant *parameters)
1024 : : {
1025 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1026 : 0 : g_autoptr (GListModel) attachments = NULL;
1027 : :
1028 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1029 : :
1030 : 0 : attachments = valent_conversation_page_get_attachments (self);
1031 : 0 : gtk_list_box_bind_model (GTK_LIST_BOX (self->attachment_list),
1032 : : attachments,
1033 : : attachment_list_create,
1034 : : NULL,
1035 : : NULL);
1036 : :
1037 [ # # ]: 0 : adw_dialog_present (self->details_dialog, widget);
1038 : 0 : }
1039 : :
1040 : : static void
1041 : 0 : gtk_file_dialog_open_multiple_cb (GtkFileDialog *dialog,
1042 : : GAsyncResult *result,
1043 : : ValentConversationPage *self)
1044 : : {
1045 : 0 : g_autoptr (GListModel) files = NULL;
1046 : 0 : unsigned int n_files;
1047 : 0 : g_autoptr (GError) error = NULL;
1048 : :
1049 : 0 : files = gtk_file_dialog_open_multiple_finish (dialog, result, &error);
1050 [ # # ]: 0 : if (files == NULL)
1051 : : {
1052 [ # # # # ]: 0 : if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED) &&
1053 : 0 : !g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
1054 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
1055 : :
1056 [ # # ]: 0 : return;
1057 : : }
1058 : :
1059 [ # # ]: 0 : if (self->attachments == NULL)
1060 : 0 : self->attachments = g_list_store_new (VALENT_TYPE_MESSAGE_ATTACHMENT);
1061 : :
1062 : 0 : n_files = g_list_model_get_n_items (files);
1063 [ # # ]: 0 : for (unsigned int i = 0; i < n_files; i++)
1064 : : {
1065 : 0 : g_autoptr (ValentMessageAttachment) attachment = NULL;
1066 [ # # ]: 0 : g_autoptr (GFile) file = NULL;
1067 : :
1068 : 0 : file = g_list_model_get_item (files, i);
1069 : 0 : attachment = g_object_new (VALENT_TYPE_MESSAGE_ATTACHMENT,
1070 : : "file", file,
1071 : : NULL);
1072 [ # # ]: 0 : g_list_store_append (self->attachments, attachment);
1073 : : }
1074 : :
1075 [ # # ]: 0 : valent_conversation_page_check_message (self);
1076 : : }
1077 : :
1078 : : /**
1079 : : * ValentSharePlugin|message.attachment:
1080 : : * @parameter: %NULL
1081 : : *
1082 : : * The default share action opens the platform-specific dialog for selecting
1083 : : * files, typically a `GtkFileChooserDialog`.
1084 : : */
1085 : : static void
1086 : 0 : message_attachment_action (GtkWidget *widget,
1087 : : const char *action_name,
1088 : : GVariant *parameters)
1089 : : {
1090 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1091 : 0 : g_autoptr (GCancellable) cancellable = NULL;
1092 : 0 : GtkFileDialog *dialog;
1093 : :
1094 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1095 : :
1096 : 0 : dialog = g_object_new (GTK_TYPE_FILE_DIALOG,
1097 : : "title", _("Attach Files"),
1098 : : "accept-label", _("Open"),
1099 : : NULL);
1100 : :
1101 : 0 : cancellable = g_cancellable_new ();
1102 : 0 : g_signal_connect_object (widget,
1103 : : "destroy",
1104 : : G_CALLBACK (g_cancellable_cancel),
1105 : : cancellable,
1106 : : G_CONNECT_SWAPPED);
1107 : :
1108 [ # # ]: 0 : gtk_file_dialog_open_multiple (dialog,
1109 : 0 : GTK_WINDOW (gtk_widget_get_root (widget)),
1110 : : cancellable,
1111 : : (GAsyncReadyCallback) gtk_file_dialog_open_multiple_cb,
1112 : : self);
1113 : 0 : }
1114 : :
1115 : : static void
1116 : 0 : message_send_action (GtkWidget *widget,
1117 : : const char *action_name,
1118 : : GVariant *parameters)
1119 : : {
1120 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (widget);
1121 : :
1122 [ # # ]: 0 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1123 : :
1124 : 0 : valent_conversation_page_send_message (self);
1125 : 0 : }
1126 : :
1127 : : /*
1128 : : * ValentConversationPage
1129 : : */
1130 : : static void
1131 : 1 : valent_conversation_page_set_iri (ValentConversationPage *self,
1132 : : const char *iri)
1133 : : {
1134 [ + - ]: 1 : g_assert (VALENT_IS_CONVERSATION_PAGE (self));
1135 [ - + - - ]: 1 : g_assert (iri == NULL || *iri != '\0');
1136 : :
1137 [ - + ]: 1 : if (g_set_str (&self->iri, iri))
1138 : 0 : valent_conversation_page_load (self);
1139 : 1 : }
1140 : :
1141 : : /*
1142 : : * AdwNavigationPage
1143 : : */
1144 : : static void
1145 : 0 : valent_conversation_page_shown (AdwNavigationPage *page)
1146 : : {
1147 : 0 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (page);
1148 : :
1149 : 0 : gtk_widget_action_set_enabled (GTK_WIDGET (self), "message.send", FALSE);
1150 : 0 : gtk_widget_grab_focus (GTK_WIDGET (self->message_entry));
1151 : :
1152 [ # # ]: 0 : if (ADW_NAVIGATION_PAGE_CLASS (valent_conversation_page_parent_class)->shown)
1153 : 0 : ADW_NAVIGATION_PAGE_CLASS (valent_conversation_page_parent_class)->shown (page);
1154 : 0 : }
1155 : :
1156 : : /*
1157 : : * GObject
1158 : : */
1159 : : static void
1160 : 1 : valent_conversation_page_dispose (GObject *object)
1161 : : {
1162 : 1 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1163 : :
1164 [ - + ]: 1 : if (self->thread != NULL)
1165 : : {
1166 : 0 : g_signal_handlers_disconnect_by_data (self->thread, self);
1167 [ # # ]: 0 : g_clear_object (&self->thread);
1168 : : }
1169 : :
1170 : 1 : gtk_widget_dispose_template (GTK_WIDGET (object),
1171 : : VALENT_TYPE_CONVERSATION_PAGE);
1172 : :
1173 : 1 : G_OBJECT_CLASS (valent_conversation_page_parent_class)->dispose (object);
1174 : 1 : }
1175 : :
1176 : : static void
1177 : 1 : valent_conversation_page_finalize (GObject *object)
1178 : : {
1179 : 1 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1180 : :
1181 [ - + ]: 1 : g_clear_object (&self->messages);
1182 [ - + ]: 1 : g_clear_object (&self->contacts);
1183 [ - + ]: 1 : g_clear_object (&self->thread);
1184 [ - + ]: 1 : g_clear_pointer (&self->iri, g_free);
1185 [ + - ]: 1 : g_clear_pointer (&self->participants, g_hash_table_unref);
1186 [ + - ]: 1 : g_clear_pointer (&self->outbox, g_hash_table_unref);
1187 [ - + ]: 1 : g_clear_object (&self->attachments);
1188 : :
1189 : 1 : G_OBJECT_CLASS (valent_conversation_page_parent_class)->finalize (object);
1190 : 1 : }
1191 : :
1192 : : static void
1193 : 3 : valent_conversation_page_get_property (GObject *object,
1194 : : guint prop_id,
1195 : : GValue *value,
1196 : : GParamSpec *pspec)
1197 : : {
1198 : 3 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1199 : :
1200 [ + + + - ]: 3 : switch ((ValentConversationPageProperty)prop_id)
1201 : : {
1202 : 1 : case PROP_CONTACTS:
1203 : 1 : g_value_set_object (value, self->contacts);
1204 : 1 : break;
1205 : :
1206 : 1 : case PROP_MESSAGES:
1207 : 1 : g_value_set_object (value, self->messages);
1208 : 1 : break;
1209 : :
1210 : 1 : case PROP_IRI:
1211 : 1 : g_value_set_string (value, self->iri);
1212 : 1 : break;
1213 : :
1214 : 0 : default:
1215 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1216 : : }
1217 : 3 : }
1218 : :
1219 : : static void
1220 : 3 : valent_conversation_page_set_property (GObject *object,
1221 : : guint prop_id,
1222 : : const GValue *value,
1223 : : GParamSpec *pspec)
1224 : : {
1225 : 3 : ValentConversationPage *self = VALENT_CONVERSATION_PAGE (object);
1226 : :
1227 [ + + + - ]: 3 : switch ((ValentConversationPageProperty)prop_id)
1228 : : {
1229 : 1 : case PROP_CONTACTS:
1230 : 1 : g_set_object (&self->contacts, g_value_get_object (value));
1231 : 1 : break;
1232 : :
1233 : 1 : case PROP_MESSAGES:
1234 : 1 : g_set_object (&self->messages, g_value_get_object (value));
1235 : 1 : break;
1236 : :
1237 : 1 : case PROP_IRI:
1238 : 1 : valent_conversation_page_set_iri (self, g_value_get_string (value));
1239 : 1 : break;
1240 : :
1241 : 0 : default:
1242 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1243 : : }
1244 : 3 : }
1245 : :
1246 : : static void
1247 : 1 : valent_conversation_page_class_init (ValentConversationPageClass *klass)
1248 : : {
1249 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
1250 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1251 : 1 : AdwNavigationPageClass *page_class = ADW_NAVIGATION_PAGE_CLASS (klass);
1252 : :
1253 : 1 : object_class->dispose = valent_conversation_page_dispose;
1254 : 1 : object_class->finalize = valent_conversation_page_finalize;
1255 : 1 : object_class->get_property = valent_conversation_page_get_property;
1256 : 1 : object_class->set_property = valent_conversation_page_set_property;
1257 : :
1258 : 1 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/gnome/valent-conversation-page.ui");
1259 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, message_list);
1260 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, message_entry);
1261 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, scrolledwindow);
1262 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, vadjustment);
1263 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, details_dialog);
1264 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, details_view);
1265 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, participant_list);
1266 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentConversationPage, attachment_list);
1267 : :
1268 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_scroll_upper_changed);
1269 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_scroll_value_changed);
1270 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_entry_activated);
1271 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_entry_changed);
1272 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_add_participant);
1273 : 1 : gtk_widget_class_install_action (widget_class, "conversation.details", NULL, conversation_details_action);
1274 : 1 : gtk_widget_class_install_action (widget_class, "message.attachment", NULL, message_attachment_action);
1275 : 1 : gtk_widget_class_install_action (widget_class, "message.send", NULL, message_send_action);
1276 : :
1277 : 1 : page_class->shown = valent_conversation_page_shown;
1278 : :
1279 : 2 : properties [PROP_CONTACTS] =
1280 : 1 : g_param_spec_object ("contacts", NULL, NULL,
1281 : : VALENT_TYPE_CONTACTS_ADAPTER,
1282 : : (G_PARAM_READWRITE |
1283 : : G_PARAM_CONSTRUCT |
1284 : : G_PARAM_STATIC_STRINGS));
1285 : :
1286 : 2 : properties [PROP_MESSAGES] =
1287 : 1 : g_param_spec_object ("messages", NULL, NULL,
1288 : : VALENT_TYPE_MESSAGES_ADAPTER,
1289 : : (G_PARAM_READWRITE |
1290 : : G_PARAM_CONSTRUCT_ONLY |
1291 : : G_PARAM_STATIC_STRINGS));
1292 : :
1293 : 2 : properties [PROP_IRI] =
1294 : 1 : g_param_spec_string ("iri", NULL, NULL,
1295 : : NULL,
1296 : : (G_PARAM_READWRITE |
1297 : : G_PARAM_CONSTRUCT_ONLY |
1298 : : G_PARAM_STATIC_STRINGS));
1299 : :
1300 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
1301 : 1 : }
1302 : :
1303 : : static uint32_t
1304 : 0 : contact_medium_hash (gconstpointer medium)
1305 : : {
1306 : 0 : g_autoptr (EPhoneNumber) number = NULL;
1307 [ # # ]: 0 : g_autofree char *number_str = NULL;
1308 : :
1309 [ # # ]: 0 : if G_UNLIKELY (g_strrstr (medium, "@"))
1310 : 0 : return g_str_hash (medium);
1311 : :
1312 : 0 : number = e_phone_number_from_string (medium, NULL, NULL);
1313 : 0 : number_str = e_phone_number_to_string (number, E_PHONE_NUMBER_FORMAT_E164);
1314 : :
1315 : 0 : return g_str_hash (number_str);
1316 : : }
1317 : :
1318 : : static gboolean
1319 : 0 : contact_medium_equal (gconstpointer medium1,
1320 : : gconstpointer medium2)
1321 : : {
1322 [ # # # # ]: 0 : if G_UNLIKELY (g_strrstr (medium1, "@") || g_strrstr (medium1, "@"))
1323 : 0 : return g_str_equal (medium1, medium2);
1324 : :
1325 : 0 : return e_phone_number_compare_strings (medium1, medium2, NULL) != E_PHONE_NUMBER_MATCH_NONE;
1326 : : }
1327 : :
1328 : : static void
1329 : 1 : valent_conversation_page_init (ValentConversationPage *self)
1330 : : {
1331 : 1 : gtk_widget_init_template (GTK_WIDGET (self));
1332 : :
1333 : 1 : gtk_list_box_set_header_func (self->message_list,
1334 : : message_list_header_func,
1335 : : self, NULL);
1336 : :
1337 : 1 : self->participants = g_hash_table_new_full (contact_medium_hash,
1338 : : contact_medium_equal,
1339 : : g_free,
1340 : : g_object_unref);
1341 : 1 : self->outbox = g_hash_table_new_full (NULL,
1342 : : NULL,
1343 : : g_object_unref,
1344 : : g_object_unref);
1345 : 1 : }
1346 : :
1347 : : /**
1348 : : * valent_conversation_page_get_iri:
1349 : : * @conversation: a `ValentConversationPage`
1350 : : *
1351 : : * Get the thread IRI for @conversation.
1352 : : *
1353 : : * Returns: the thread IRI
1354 : : */
1355 : : const char *
1356 : 1 : valent_conversation_page_get_iri (ValentConversationPage *conversation)
1357 : : {
1358 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_CONVERSATION_PAGE (conversation), NULL);
1359 : :
1360 : 1 : return conversation->iri;
1361 : : }
1362 : :
1363 : : /**
1364 : : * valent_conversation_page_add_participant:
1365 : : * @conversation: a `ValentConversationPage`
1366 : : * @contact: an `EContact`
1367 : : * @medium: a contact IRI
1368 : : *
1369 : : * Add @contact to @conversation, with the contact point @medium.
1370 : : */
1371 : : void
1372 : 0 : valent_conversation_page_add_participant (ValentConversationPage *conversation,
1373 : : EContact *contact,
1374 : : const char *medium)
1375 : : {
1376 : 0 : GHashTableIter iter;
1377 : 0 : GtkWidget *child;
1378 : 0 : size_t position = 0;
1379 : 0 : gboolean is_new = FALSE;
1380 : :
1381 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (conversation));
1382 [ # # # # : 0 : g_return_if_fail (E_IS_CONTACT (contact));
# # # # ]
1383 [ # # # # ]: 0 : g_return_if_fail (medium != NULL && *medium != '\0');
1384 : :
1385 : : // FIXME: use vmo:hasParticipant
1386 [ # # ]: 0 : is_new = g_hash_table_replace (conversation->participants,
1387 : 0 : g_strdup (medium),
1388 : : g_object_ref (contact));
1389 [ # # ]: 0 : if (!is_new)
1390 : : return;
1391 : :
1392 : : /* Clear the dialog
1393 : : */
1394 : 0 : child = gtk_widget_get_first_child (conversation->participant_list);
1395 [ # # ]: 0 : while (child != NULL)
1396 : : {
1397 : 0 : gtk_list_box_remove (GTK_LIST_BOX (conversation->participant_list), child);
1398 : 0 : child = gtk_widget_get_first_child (conversation->participant_list);
1399 : : }
1400 : :
1401 : : /* Update the dialog
1402 : : */
1403 : 0 : g_hash_table_iter_init (&iter, conversation->participants);
1404 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (void **)&medium, (void **)&contact))
1405 : : {
1406 : 0 : const char *name = NULL;
1407 : :
1408 : 0 : name = e_contact_get_const (contact, E_CONTACT_FULL_NAME);
1409 : 0 : adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (conversation), name);
1410 : :
1411 : 0 : child = g_object_new (VALENT_TYPE_CONTACT_ROW,
1412 : : "contact", contact,
1413 : : "contact-medium", medium,
1414 : : NULL);
1415 : 0 : gtk_list_box_insert (GTK_LIST_BOX (conversation->participant_list),
1416 : : child,
1417 : 0 : position++);
1418 : : }
1419 : : }
1420 : :
1421 : : static void
1422 : 0 : valent_conversation_page_scroll_to_row (ValentConversationPage *self,
1423 : : GtkWidget *row)
1424 : : {
1425 : 0 : GtkWidget *viewport;
1426 : 0 : double upper, page_size;
1427 : 0 : double target, maximum;
1428 : :
1429 : 0 : upper = gtk_adjustment_get_upper (self->vadjustment);
1430 : 0 : page_size = gtk_adjustment_get_page_size (self->vadjustment);
1431 : 0 : maximum = upper - page_size;
1432 : 0 : target = upper - page_size;
1433 : :
1434 [ # # ]: 0 : if (row != NULL)
1435 : : {
1436 : 0 : graphene_rect_t row_bounds;
1437 : 0 : graphene_point_t row_point;
1438 : 0 : graphene_point_t target_point;
1439 : :
1440 : 0 : viewport = gtk_scrolled_window_get_child (self->scrolledwindow);
1441 [ # # ]: 0 : if (!gtk_widget_compute_bounds (row, viewport, &row_bounds))
1442 : : {
1443 : 0 : g_warning ("%s(): failed to scroll to row", G_STRFUNC);
1444 : 0 : return;
1445 : : }
1446 : :
1447 : 0 : graphene_rect_get_bottom_right (&row_bounds, &row_point);
1448 [ # # ]: 0 : if (!gtk_widget_compute_point (row, viewport, &row_point, &target_point))
1449 : : {
1450 : 0 : g_warning ("%s(): failed to scroll to row", G_STRFUNC);
1451 : 0 : return;
1452 : : }
1453 : :
1454 : 0 : target = target_point.y;
1455 : : }
1456 : :
1457 : 0 : gtk_scrolled_window_set_kinetic_scrolling (self->scrolledwindow, FALSE);
1458 [ # # ]: 0 : gtk_adjustment_set_value (self->vadjustment, CLAMP (target, 0, maximum));
1459 : 0 : gtk_scrolled_window_set_kinetic_scrolling (self->scrolledwindow, TRUE);
1460 : : }
1461 : :
1462 : : /**
1463 : : * valent_conversation_page_scroll_to_date:
1464 : : * @page: a `ValentConversationPage`
1465 : : * @date: a UNIX epoch timestamp
1466 : : *
1467 : : * Scroll to the message closest to @date.
1468 : : */
1469 : : void
1470 : 0 : valent_conversation_page_scroll_to_date (ValentConversationPage *page,
1471 : : int64_t date)
1472 : : {
1473 : 0 : GtkWidget *row;
1474 : 0 : ValentMessage *message;
1475 : :
1476 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (page));
1477 [ # # ]: 0 : g_return_if_fail (date > 0);
1478 : :
1479 : : /* First look through the list box */
1480 : 0 : for (row = gtk_widget_get_last_child (GTK_WIDGET (page->message_list));
1481 [ # # ]: 0 : row != NULL;
1482 : 0 : row = gtk_widget_get_prev_sibling (row))
1483 : : {
1484 : : /* If this message is equal or older than the target date, we're done
1485 : : */
1486 [ # # ]: 0 : if (valent_conversation_row_get_date (VALENT_CONVERSATION_ROW (row)) <= date)
1487 : : {
1488 : 0 : valent_conversation_page_scroll_to_row (page, row);
1489 : 0 : return;
1490 : : }
1491 : : }
1492 : :
1493 : : /* If there are no more messages, we're done
1494 : : */
1495 [ # # ]: 0 : g_return_if_fail (G_IS_LIST_MODEL (page->thread));
1496 : :
1497 : : /* Populate the list in reverse until we find the message
1498 : : */
1499 [ # # ]: 0 : while ((message = valent_conversation_page_pop_tail (page)) != NULL)
1500 : : {
1501 : : /* Prepend the message
1502 : : */
1503 : 0 : row = valent_conversation_page_insert_message (page, message, 0);
1504 : 0 : g_object_unref (message);
1505 : :
1506 : : /* If this message is equal or older than the target date, we're done
1507 : : */
1508 [ # # ]: 0 : if (valent_message_get_date (message) <= date)
1509 : : {
1510 : 0 : valent_conversation_page_scroll_to_row (page, row);
1511 : 0 : return;
1512 : : }
1513 : : }
1514 : : }
1515 : :
1516 : : /**
1517 : : * valent_conversation_page_scroll_to_message:
1518 : : * @page: a `ValentConversationPage`
1519 : : * @message: a `ValentMessage`
1520 : : *
1521 : : * A convenience for calling valent_message_get_date() and then
1522 : : * valent_conversation_page_scroll_to_date().
1523 : : */
1524 : : void
1525 : 0 : valent_conversation_page_scroll_to_message (ValentConversationPage *page,
1526 : : ValentMessage *message)
1527 : : {
1528 : 0 : int64_t date;
1529 : :
1530 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONVERSATION_PAGE (page));
1531 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGE (message));
1532 : :
1533 : 0 : date = valent_message_get_date (message);
1534 : 0 : valent_conversation_page_scroll_to_date (page, date);
1535 : : }
1536 : :
|