Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-sms-conversation-row"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <adwaita.h>
9 : : #include <gtk/gtk.h>
10 : : #include <pango/pango.h>
11 : : #include <valent.h>
12 : :
13 : : #include "valent-message.h"
14 : : #include "valent-sms-conversation-row.h"
15 : : #include "valent-sms-utils.h"
16 : :
17 : :
18 : : struct _ValentSmsConversationRow
19 : : {
20 : : GtkListBoxRow parent_instance;
21 : :
22 : : ValentMessage *message;
23 : : EContact *contact;
24 : : unsigned int incoming : 1;
25 : :
26 : : GtkWidget *grid;
27 : : GtkWidget *avatar;
28 : : GtkWidget *bubble;
29 : : GtkWidget *text_label;
30 : : };
31 : :
32 [ + + + - ]: 70 : G_DEFINE_FINAL_TYPE (ValentSmsConversationRow, valent_sms_conversation_row, GTK_TYPE_LIST_BOX_ROW)
33 : :
34 : :
35 : : enum {
36 : : PROP_0,
37 : : PROP_CONTACT,
38 : : PROP_DATE,
39 : : PROP_MESSAGE,
40 : : N_PROPERTIES
41 : : };
42 : :
43 : : static GParamSpec *properties[N_PROPERTIES] = { NULL, };
44 : :
45 : :
46 : : /* LCOV_EXCL_START */
47 : : static gboolean
48 : : valent_sms_conversation_row_activate_link (GtkLabel *label,
49 : : const char *uri,
50 : : gpointer user_data)
51 : : {
52 : : GtkWidget *widget = GTK_WIDGET (label);
53 : : GtkWindow *toplevel = GTK_WINDOW (gtk_widget_get_root (widget));
54 : : g_autoptr (GtkUriLauncher) launcher = NULL;
55 : : g_autofree char *url = NULL;
56 : :
57 : : /* Only handle links that need to be amended with a scheme */
58 : : if (g_uri_peek_scheme (uri) != NULL)
59 : : return FALSE;
60 : :
61 : : if (!GTK_IS_WINDOW (toplevel))
62 : : return FALSE;
63 : :
64 : : url = g_strdup_printf ("https://%s", uri);
65 : : launcher = gtk_uri_launcher_new (url);
66 : : gtk_uri_launcher_launch (launcher, toplevel, NULL, NULL, NULL);
67 : :
68 : : return TRUE;
69 : : }
70 : : /* LCOV_EXCL_STOP */
71 : :
72 : : /*
73 : : * GObject
74 : : */
75 : : static void
76 : 4 : valent_sms_conversation_row_finalize (GObject *object)
77 : : {
78 : 4 : ValentSmsConversationRow *self = VALENT_SMS_CONVERSATION_ROW (object);
79 : :
80 [ + + ]: 4 : g_clear_object (&self->contact);
81 : :
82 [ + - ]: 4 : if (self->message)
83 : : {
84 : 4 : g_signal_handlers_disconnect_by_data (self->message, self);
85 [ + - ]: 4 : g_clear_object (&self->message);
86 : : }
87 : :
88 : 4 : G_OBJECT_CLASS (valent_sms_conversation_row_parent_class)->finalize (object);
89 : 4 : }
90 : :
91 : : static void
92 : 3 : valent_sms_conversation_row_get_property (GObject *object,
93 : : guint prop_id,
94 : : GValue *value,
95 : : GParamSpec *pspec)
96 : : {
97 : 3 : ValentSmsConversationRow *self = VALENT_SMS_CONVERSATION_ROW (object);
98 : :
99 [ + + + - ]: 3 : switch (prop_id)
100 : : {
101 : 1 : case PROP_CONTACT:
102 : 1 : g_value_set_object (value, self->contact);
103 : 1 : break;
104 : :
105 : 1 : case PROP_DATE:
106 : 1 : g_value_set_int64 (value, valent_sms_conversation_row_get_date (self));
107 : 1 : break;
108 : :
109 : 1 : case PROP_MESSAGE:
110 : 1 : g_value_set_object (value, self->message);
111 : 1 : break;
112 : :
113 : 0 : default:
114 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
115 : : }
116 : 3 : }
117 : :
118 : : static void
119 : 10 : valent_sms_conversation_row_set_property (GObject *object,
120 : : guint prop_id,
121 : : const GValue *value,
122 : : GParamSpec *pspec)
123 : : {
124 : 10 : ValentSmsConversationRow *self = VALENT_SMS_CONVERSATION_ROW (object);
125 : :
126 [ + + - ]: 10 : switch (prop_id)
127 : : {
128 : 5 : case PROP_CONTACT:
129 : 5 : valent_sms_conversation_row_set_contact (self, g_value_get_object (value));
130 : 5 : break;
131 : :
132 : 5 : case PROP_MESSAGE:
133 : 5 : valent_sms_conversation_row_set_message (self, g_value_get_object (value));
134 : 5 : break;
135 : :
136 : 0 : default:
137 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
138 : : }
139 : 10 : }
140 : :
141 : : static void
142 : 3 : valent_sms_conversation_row_class_init (ValentSmsConversationRowClass *klass)
143 : : {
144 : 3 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
145 : :
146 : 3 : object_class->finalize = valent_sms_conversation_row_finalize;
147 : 3 : object_class->get_property = valent_sms_conversation_row_get_property;
148 : 3 : object_class->set_property = valent_sms_conversation_row_set_property;
149 : :
150 : : /**
151 : : * ValentSmsConversationRow:contact
152 : : *
153 : : * The `EContact` that sent this message.
154 : : */
155 : 6 : properties [PROP_CONTACT] =
156 : 3 : g_param_spec_object ("contact", NULL, NULL,
157 : : E_TYPE_CONTACT,
158 : : (G_PARAM_READWRITE |
159 : : G_PARAM_CONSTRUCT |
160 : : G_PARAM_EXPLICIT_NOTIFY |
161 : : G_PARAM_STATIC_STRINGS));
162 : :
163 : : /**
164 : : * ValentSmsConversationRow:date
165 : : *
166 : : * The timestamp of the message.
167 : : */
168 : 6 : properties [PROP_DATE] =
169 : 3 : g_param_spec_int64 ("date", NULL, NULL,
170 : : 0, G_MAXINT64,
171 : : 0,
172 : : (G_PARAM_READABLE |
173 : : G_PARAM_EXPLICIT_NOTIFY |
174 : : G_PARAM_STATIC_STRINGS));
175 : :
176 : : /**
177 : : * ValentSmsConversationRow:message
178 : : *
179 : : * The message this row displays.
180 : : */
181 : 6 : properties [PROP_MESSAGE] =
182 : 3 : g_param_spec_object ("message", NULL, NULL,
183 : : VALENT_TYPE_MESSAGE,
184 : : (G_PARAM_READWRITE |
185 : : G_PARAM_CONSTRUCT |
186 : : G_PARAM_EXPLICIT_NOTIFY |
187 : : G_PARAM_STATIC_STRINGS));
188 : :
189 : 3 : g_object_class_install_properties (object_class, N_PROPERTIES, properties);
190 : 3 : }
191 : :
192 : : static void
193 : 5 : valent_sms_conversation_row_init (ValentSmsConversationRow *self)
194 : : {
195 : 5 : gtk_widget_add_css_class (GTK_WIDGET (self), "valent-sms-conversation-row");
196 : :
197 : 5 : self->grid = g_object_new (GTK_TYPE_GRID,
198 : : "can-focus", FALSE,
199 : : "column-spacing", 6,
200 : : "hexpand", TRUE,
201 : : "margin-start", 6,
202 : : "margin-end", 6,
203 : : "margin-top", 6,
204 : : "margin-bottom", 6,
205 : : NULL);
206 : 5 : gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (self), self->grid);
207 : :
208 : : /* Contact Avatar */
209 : 5 : self->avatar = g_object_new (ADW_TYPE_AVATAR,
210 : : "size", 32,
211 : : "halign", GTK_ALIGN_START,
212 : : "valign", GTK_ALIGN_END,
213 : : "vexpand", TRUE,
214 : : "visible", FALSE,
215 : : NULL);
216 : 5 : gtk_grid_attach (GTK_GRID (self->grid), self->avatar, 0, 0, 1, 1);
217 : :
218 : : /* Message Layout */
219 : 5 : self->bubble = g_object_new (GTK_TYPE_GRID, NULL);
220 : 5 : gtk_grid_attach (GTK_GRID (self->grid), self->bubble, 1, 0, 1, 1);
221 : :
222 : : /* Message Text */
223 : 5 : self->text_label = g_object_new (GTK_TYPE_LABEL,
224 : : "halign", GTK_ALIGN_START,
225 : : "use-markup", TRUE,
226 : : "selectable", TRUE,
227 : : "wrap", TRUE,
228 : : "wrap-mode", PANGO_WRAP_WORD_CHAR,
229 : : "xalign", 0.0,
230 : : NULL);
231 : 5 : gtk_widget_set_can_focus (self->text_label, FALSE);
232 : 5 : gtk_grid_attach (GTK_GRID (self->bubble), self->text_label, 0, 0, 1, 1);
233 : :
234 : : /* Catch activate-link to fixup URIs without a scheme */
235 : 5 : g_signal_connect (self->text_label,
236 : : "activate-link",
237 : : G_CALLBACK (valent_sms_conversation_row_activate_link),
238 : : NULL);
239 : 5 : }
240 : :
241 : : /**
242 : : * valent_sms_conversation_row_new:
243 : : * @message: a `ValentMessage`
244 : : * @contact: a `EContact`
245 : : *
246 : : * Create a new conversation message for @contact and @message.
247 : : *
248 : : * Returns: (transfer full): a `ValentSmsConversationRow`
249 : : */
250 : : GtkWidget *
251 : 1 : valent_sms_conversation_row_new (ValentMessage *message,
252 : : EContact *contact)
253 : : {
254 : 1 : return g_object_new (VALENT_TYPE_SMS_CONVERSATION_ROW,
255 : : "contact", contact,
256 : : "message", message,
257 : : NULL);
258 : : }
259 : :
260 : : /**
261 : : * valent_sms_conversation_row_get_contact:
262 : : * @row: a `ValentSmsConversationRow`
263 : : *
264 : : * Get the contact.
265 : : *
266 : : * Returns: (transfer none) (nullable): a `ValentContact`
267 : : */
268 : : EContact *
269 : 1 : valent_sms_conversation_row_get_contact (ValentSmsConversationRow *row)
270 : : {
271 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row), NULL);
272 : :
273 : 1 : return row->contact;
274 : : }
275 : :
276 : : /**
277 : : * valent_sms_conversation_row_set_contact:
278 : : * @row: a `ValentSmsConversationRow`
279 : : * @contact: a `ValentContact`
280 : : *
281 : : * Set or update the contact.
282 : : */
283 : : void
284 : 6 : valent_sms_conversation_row_set_contact (ValentSmsConversationRow *row,
285 : : EContact *contact)
286 : : {
287 [ + - ]: 6 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row));
288 [ + + + - : 6 : g_return_if_fail (contact == NULL || E_IS_CONTACT (contact));
- + - - ]
289 : :
290 [ + + ]: 6 : if (!g_set_object (&row->contact, contact))
291 : : return;
292 : :
293 [ + - ]: 2 : if (row->contact != NULL)
294 : 2 : valent_sms_avatar_from_contact (ADW_AVATAR (row->avatar), contact);
295 : :
296 : 2 : valent_sms_conversation_row_update (row);
297 : 2 : g_object_notify_by_pspec (G_OBJECT (row), properties [PROP_CONTACT]);
298 : : }
299 : :
300 : : /**
301 : : * valent_sms_conversation_row_get_date:
302 : : * @row: a `ValentSmsConversationRow`
303 : : *
304 : : * Get the timestamp of the message.
305 : : *
306 : : * Returns: a UNIX epoch timestamp
307 : : */
308 : : int64_t
309 : 10 : valent_sms_conversation_row_get_date (ValentSmsConversationRow *row)
310 : : {
311 [ + - ]: 10 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row), 0);
312 : :
313 [ - + ]: 10 : if G_UNLIKELY (row->message == NULL)
314 : : return 0;
315 : :
316 : 10 : return valent_message_get_date (row->message);
317 : : }
318 : :
319 : : /**
320 : : * valent_sms_conversation_row_get_id:
321 : : * @row: a `ValentSmsConversationRow`
322 : : *
323 : : * Get the ID of the message.
324 : : *
325 : : * Returns: a message id
326 : : */
327 : : int64_t
328 : 1 : valent_sms_conversation_row_get_id (ValentSmsConversationRow *row)
329 : : {
330 [ + - ]: 1 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row), 0);
331 : :
332 [ - + ]: 1 : if G_UNLIKELY (row->message == NULL)
333 : : return 0;
334 : :
335 : 1 : return valent_message_get_id (row->message);
336 : : }
337 : :
338 : : /**
339 : : * valent_sms_conversation_row_get_message:
340 : : * @row: a `ValentSmsConversationRow`
341 : : *
342 : : * Get the message.
343 : : *
344 : : * Returns: (transfer none): a `ValentMessage`
345 : : */
346 : : ValentMessage *
347 : 2 : valent_sms_conversation_row_get_message (ValentSmsConversationRow *row)
348 : : {
349 [ + - ]: 2 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row), NULL);
350 : :
351 : 2 : return row->message;
352 : : }
353 : :
354 : : /**
355 : : * valent_sms_conversation_row_set_message:
356 : : * @row: a `ValentSmsConversationRow`
357 : : * @message: a `ValentMessage`
358 : : *
359 : : * Set or update the message.
360 : : */
361 : : void
362 : 5 : valent_sms_conversation_row_set_message (ValentSmsConversationRow *row,
363 : : ValentMessage *message)
364 : : {
365 [ + - ]: 5 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row));
366 [ + - - + ]: 5 : g_return_if_fail (message == NULL || VALENT_IS_MESSAGE (message));
367 : :
368 [ + - ]: 5 : if (row->message == message)
369 : : return;
370 : :
371 [ - + ]: 5 : if (row->message != NULL)
372 : : {
373 : 0 : g_signal_handlers_disconnect_by_data (row->message, row);
374 [ # # ]: 0 : g_clear_object (&row->message);
375 : : }
376 : :
377 [ + - ]: 5 : if (message != NULL)
378 : : {
379 : 5 : row->message = g_object_ref (message);
380 : 5 : g_signal_connect_swapped (row->message,
381 : : "notify",
382 : : G_CALLBACK (valent_sms_conversation_row_update),
383 : : row);
384 : :
385 : 5 : valent_sms_conversation_row_update (row);
386 : 5 : g_object_notify_by_pspec (G_OBJECT (row), properties [PROP_MESSAGE]);
387 : : }
388 : : }
389 : :
390 : : /**
391 : : * valent_sms_conversation_row_is_incoming:
392 : : * @row: a `ValentSmsConversationRow`
393 : : *
394 : : * Update @row based on the current values of `ValentSmsConversation`:message.
395 : : */
396 : : gboolean
397 : 17 : valent_sms_conversation_row_is_incoming (ValentSmsConversationRow *row)
398 : : {
399 [ + - ]: 17 : g_return_val_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row), FALSE);
400 : :
401 [ - + ]: 17 : if (row->message == NULL)
402 : : return FALSE;
403 : :
404 : 17 : return row->incoming;
405 : : }
406 : :
407 : : /**
408 : : * valent_sms_conversation_row_show_avatar:
409 : : * @row: a `ValentSmsConversationRow`
410 : : * @visible: Whether to show the avatar
411 : : *
412 : : * Show or hide the contact avatar for @row, updating the margins accordingly.
413 : : */
414 : : void
415 : 2 : valent_sms_conversation_row_show_avatar (ValentSmsConversationRow *row,
416 : : gboolean visible)
417 : : {
418 [ + - ]: 2 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row));
419 : :
420 [ + - ]: 2 : if G_LIKELY (gtk_widget_get_visible (row->avatar) == visible)
421 : : return;
422 : :
423 [ + + ]: 2 : if (visible)
424 : : {
425 : 1 : gtk_widget_set_margin_start (row->bubble, 6);
426 : 1 : gtk_widget_set_margin_bottom (row->bubble, 6);
427 : : }
428 : : else
429 : : {
430 : 1 : gtk_widget_set_margin_start (row->bubble, 44);
431 : 1 : gtk_widget_set_margin_bottom (row->bubble, 0);
432 : : }
433 : :
434 : 2 : gtk_widget_set_visible (row->avatar, visible);
435 : : }
436 : :
437 : : /**
438 : : * valent_sms_conversation_row_update:
439 : : * @row: a `ValentSmsConversationRow`
440 : : *
441 : : * Update @row based on the current values of `ValentSmsConversation`:message.
442 : : */
443 : : void
444 : 15 : valent_sms_conversation_row_update (ValentSmsConversationRow *row)
445 : : {
446 [ + - ]: 15 : g_return_if_fail (VALENT_IS_SMS_CONVERSATION_ROW (row));
447 : :
448 : : // text
449 [ + + ]: 15 : if (row->message == NULL)
450 : 1 : row->incoming = FALSE;
451 : : else
452 : 14 : row->incoming = valent_message_get_box (row->message) == VALENT_MESSAGE_BOX_INBOX;
453 : :
454 : 15 : gtk_widget_set_visible (row->avatar, row->incoming);
455 : :
456 : : /* Message Body */
457 [ + + ]: 15 : if (row->message != NULL)
458 : : {
459 : 14 : const char *text;
460 : 14 : g_autofree char *label = NULL;
461 : :
462 : 14 : text = valent_message_get_text (row->message);
463 : 14 : label = valent_string_to_markup (text);
464 : 14 : gtk_label_set_label (GTK_LABEL (row->text_label), label);
465 : : }
466 : :
467 : : /* The row style consists of the alignment and the CSS
468 : : *
469 : : * The row margin opposite the avatar is chosen to balance the row. Outgoing
470 : : * messages don't show avatars, so get double the margin (88px):
471 : : *
472 : : * 44px (margin) = 6px (margin) + 32px (avatar) + 6px (spacing)
473 : : *
474 : : * The CSS classes determine the chat bubble style and color.
475 : : */
476 [ + + ]: 15 : if (row->incoming)
477 : : {
478 : 5 : gtk_widget_set_halign (row->grid, GTK_ALIGN_START);
479 : 5 : gtk_widget_set_margin_end (row->grid, 44);
480 : 5 : gtk_widget_set_margin_start (row->grid, 6);
481 : :
482 : 5 : gtk_widget_remove_css_class (row->bubble, "valent-sms-outgoing");
483 : 5 : gtk_widget_add_css_class (row->bubble, "valent-sms-incoming");
484 : 5 : gtk_widget_set_halign (GTK_WIDGET (row), GTK_ALIGN_START);
485 : : }
486 : : else
487 : : {
488 : 10 : gtk_widget_set_halign (row->grid, GTK_ALIGN_END);
489 : 10 : gtk_widget_set_margin_end (row->grid, 6);
490 : 10 : gtk_widget_set_margin_start (row->grid, 88);
491 : :
492 : 10 : gtk_widget_remove_css_class (row->bubble, "valent-sms-incoming");
493 : 10 : gtk_widget_add_css_class (row->bubble, "valent-sms-outgoing");
494 : 10 : gtk_widget_set_halign (GTK_WIDGET (row), GTK_ALIGN_END);
495 : : }
496 : : }
497 : :
|