LCOV - code coverage report
Current view: top level - src/plugins/gnome - valent-conversation-page.c (source / functions) Coverage Total Hit
Test: Code Coverage Lines: 16.4 % 641 105
Test Date: 2024-12-06 20:21:07 Functions: 26.9 % 52 14
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 6.3 % 382 24

             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                 :             : 
        

Generated by: LCOV version 2.0-1