conversationsview.cpp 29.7 KB
Newer Older
1
/****************************************************************************
2
 *    Copyright (C) 2017-2019 Savoir-faire Linux Inc.                             *
3 4
 *   Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>             *
 *   Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>           *
5
 *   Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>             *
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 *                                                                          *
 *   This library is free software; you can redistribute it and/or          *
 *   modify it under the terms of the GNU Lesser General Public             *
 *   License as published by the Free Software Foundation; either           *
 *   version 2.1 of the License, or (at your option) any later version.     *
 *                                                                          *
 *   This library is distributed in the hope that it will be useful,        *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of         *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU      *
 *   Lesser General Public License for more details.                        *
 *                                                                          *
 *   You should have received a copy of the GNU General Public License      *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
 ***************************************************************************/

#include "conversationsview.h"

// std
#include <algorithm>
25 26 27 28
#include <chrono>
#include <iomanip> // for std::put_time
#include <string>
#include <sstream>
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

// GTK+ related
#include <QSize>

// LRC
#include <globalinstances.h>
#include <api/conversationmodel.h>
#include <api/contactmodel.h>
#include <api/call.h>
#include <api/contact.h>
#include <api/newcallmodel.h>

// Gnome client
#include "native/pixbufmanipulator.h"
#include "conversationpopupmenu.h"

static constexpr const char* CALL_TARGET    = "CALL_TARGET";
static constexpr int         CALL_TARGET_ID = 0;

struct _ConversationsView
{
    GtkTreeView parent;
};

struct _ConversationsViewClass
{
    GtkTreeViewClass parent_class;
};

typedef struct _ConversationsViewPrivate ConversationsViewPrivate;

struct _ConversationsViewPrivate
{
62
    AccountInfoPointer const *accountInfo_;
63 64 65

    GtkWidget* popupMenu_;

66 67
    bool useDarkTheme {false};

68 69 70
    QMetaObject::Connection selection_updated;
    QMetaObject::Connection layout_changed;
    QMetaObject::Connection modelSortedConnection_;
71
    QMetaObject::Connection conversationUpdatedConnection_;
72
    QMetaObject::Connection filterChangedConnection_;
73
    QMetaObject::Connection callChangedConnection_;
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
};

G_DEFINE_TYPE_WITH_PRIVATE(ConversationsView, conversations_view, GTK_TYPE_TREE_VIEW);

#define CONVERSATIONS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CONVERSATIONS_VIEW_TYPE, ConversationsViewPrivate))

static void
render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
                     GtkCellRenderer *cell,
                     GtkTreeModel *model,
                     GtkTreeIter *iter,
                     gpointer self)
{
    // Get active conversation
    auto path = gtk_tree_model_get_path(model, iter);
    auto row = std::atoi(gtk_tree_path_to_string(path));
    if (row == -1) return;
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    if (!priv) return;
    try
    {
        // Draw first contact.
        // NOTE: We just draw the first contact, must change this for conferences when they will have their own object
97 98
        auto conversationInfo = (*priv->accountInfo_)->conversationModel->filteredConversation(row);
        auto contactInfo = (*priv->accountInfo_)->contactModel->getContact(conversationInfo.participants.front());
99 100
        std::shared_ptr<GdkPixbuf> image;
        auto var_photo = GlobalInstances::pixmapManipulator().conversationPhoto(
101
            conversationInfo,
102
            **(priv->accountInfo_),
103
            QSize(50, 50),
104
            contactInfo.isPresent
105 106 107 108 109 110 111
        );
        image = var_photo.value<std::shared_ptr<GdkPixbuf>>();

        // set the width of the cell rendered to the width of the photo
        // so that the other renderers are shifted to the right
        g_object_set(G_OBJECT(cell), "width", 50, NULL);
        g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
112 113

        // Banned contacts should be displayed with grey bg
114
        g_object_set(G_OBJECT(cell), "cell-background", contactInfo.isBanned ? "#BDBDBD" : NULL, NULL);
115 116 117 118 119 120 121 122 123 124 125 126
    }
    catch (const std::exception&)
    {
        g_warning("Can't get conversation at row %i", row);
    }
}

static void
render_name_and_last_interaction(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
                                 GtkCellRenderer *cell,
                                 GtkTreeModel *model,
                                 GtkTreeIter *iter,
127
                                 GtkTreeView *treeview)
128 129 130 131 132 133
{
    gchar *alias;
    gchar *registeredName;
    gchar *lastInteraction;
    gchar *text;
    gchar *uid;
134
    gchar *uri;
135

136 137 138 139 140 141 142 143 144 145 146 147
    // Get active conversation
    auto path = gtk_tree_model_get_path(model, iter);
    auto row = std::atoi(gtk_tree_path_to_string(path));
    if (row == -1) return;

    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(treeview);
    if (!priv) return;

    auto conversation = (*priv->accountInfo_)->conversationModel->filteredConversation(row);
    auto contactUri = conversation.participants.front();
    auto contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);

148 149
    auto grey = priv->useDarkTheme? "#bbb" : "#666";

150 151 152
    gtk_tree_model_get (model, iter,
                        0 /* col# */, &uid /* data */,
                        1 /* col# */, &alias /* data */,
153 154 155
                        2 /* col# */, &uri /* data */,
                        3 /* col# */, &registeredName /* data */,
                        5 /* col# */, &lastInteraction /* data */,
156 157
                        -1);

158
    auto bestId = std::string(registeredName).empty() ? uri: registeredName;
159 160 161 162 163 164 165
    if (contactInfo.isBanned) {
        // Contact is banned, display it clearly
        text = g_markup_printf_escaped(
            "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\" font_weight=\"bold\">Banned contact</span>",
            bestId
        );
    } else if (std::string(alias).empty()) {
166
        // If no alias to show, use the best id
167
        text = g_markup_printf_escaped(
168
            "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\" color=\"%s\">%s</span>",
169
            bestId,
170
            grey,
171 172
            lastInteraction
        );
173 174
    } else if (std::string(alias) == std::string(bestId)) {
        // If the alias and the best id are identical, show only the alias
175
        text = g_markup_printf_escaped(
176
            "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\" color=\"%s\">%s</span>",
177
            alias,
178
            grey,
179 180 181
            lastInteraction
        );
    } else {
182
        // If the alias is not empty and not equals to the best id, show both the alias and the best id
183
        text = g_markup_printf_escaped(
184
            "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\" color=\"%s\">%s</span>\n<span size=\"smaller\" color=\"%s\">%s</span>",
185
            alias,
186
            grey,
187
            bestId,
188
            grey,
189 190 191 192
            lastInteraction
        );
    }

193
    // Banned contacts should be displayed with grey bg
194
    g_object_set(G_OBJECT(cell), "cell-background", contactInfo.isBanned ? "#BDBDBD" : NULL, NULL);
195

196
    g_object_set(G_OBJECT(cell), "markup", text, NULL);
197
    g_free(text);
198
    g_free(uid);
199
    g_free(uri);
200 201 202 203 204 205 206 207 208 209 210
    g_free(alias);
    g_free(registeredName);
}

static void
render_time(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
            GtkCellRenderer *cell,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            G_GNUC_UNUSED GtkTreeView *treeview)
{
211 212 213
    g_return_if_fail(IS_CONVERSATIONS_VIEW(treeview));
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(treeview);
    g_return_if_fail(priv);
214 215 216 217

    // Get active conversation
    auto path = gtk_tree_model_get_path(model, iter);
    auto row = std::atoi(gtk_tree_path_to_string(path));
218
    g_return_if_fail(row != -1);
219

220
    try {
221
        auto conversation = (*priv->accountInfo_)->conversationModel->filteredConversation(row);
222
        auto contactUri = conversation.participants.front();
223
        auto& contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);
224 225

        // Banned contacts should be displayed with grey bg
226
        g_object_set(G_OBJECT(cell), "cell-background", contactInfo.isBanned ? "#BDBDBD" : NULL, NULL);
227

228 229
        auto callId = conversation.confId.empty() ? conversation.callId : conversation.confId;
        if (!callId.empty()) {
230
            auto call = (*priv->accountInfo_)->callModel->getCall(callId);
231 232 233
            if (call.status != lrc::api::call::Status::ENDED) {
                g_object_set(G_OBJECT(cell), "markup", lrc::api::call::to_string(call.status).c_str(), NULL);
                return;
234 235
            }
        }
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254

        auto& interactions = conversation.interactions;
        auto lastUid = conversation.lastMessageUid;

        if (!interactions.empty() && interactions.find(lastUid) != interactions.end()) {
            std::time_t lastTimestamp = interactions[lastUid].timestamp;
            std::chrono::time_point<std::chrono::system_clock> lastTs = std::chrono::system_clock::from_time_t(lastTimestamp);
            std::chrono::time_point<std::chrono::system_clock> now = std::chrono::system_clock::now();
            std::chrono::hours diff = std::chrono::duration_cast<std::chrono::hours>(now - lastTs);

            std::stringstream timestamp;
            timestamp << std::put_time(std::localtime(&lastTimestamp), diff.count() < 24 ? "%R" : "%x");
            gchar* text = g_markup_printf_escaped("<span size=\"smaller\" color=\"#666\">%s</span>", timestamp.str().c_str());
            g_object_set(G_OBJECT(cell), "markup", text, NULL);

            g_free(text);
            return;
        }
    } catch (const std::exception&) {
255 256 257
        g_warning("Can't get conversation at row %i", row);
    }

258
    g_object_set(G_OBJECT(cell), "markup", "", NULL);
259 260
}

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
void
update_conversation(ConversationsView *self, const std::string& uid) {
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(self));

    auto idx = 0;
    auto iterIsCorrect = true;
    GtkTreeIter iter;

    while(iterIsCorrect) {
        iterIsCorrect = gtk_tree_model_iter_nth_child (model, &iter, nullptr, idx);
        if (!iterIsCorrect)
            break;
        gchar *ringId;
        gtk_tree_model_get (model, &iter,
                            0 /* col# */, &ringId /* data */,
                            -1);
        if(std::string(ringId) == uid) {
            // Get informations
280
            auto conversation = (*priv->accountInfo_)->conversationModel->filteredConversation(idx);
281
            auto contactUri = conversation.participants.front();
282
            auto contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
            auto lastMessage = conversation.interactions.empty() ? "" :
                conversation.interactions.at(conversation.lastMessageUid).body;
            std::replace(lastMessage.begin(), lastMessage.end(), '\n', ' ');
            auto alias = contactInfo.profileInfo.alias;
            alias.erase(std::remove(alias.begin(), alias.end(), '\r'), alias.end());
            // Update iter
            gtk_list_store_set (GTK_LIST_STORE(model), &iter,
                                0 /* col # */ , conversation.uid.c_str() /* celldata */,
                                1 /* col # */ , alias.c_str() /* celldata */,
                                2 /* col # */ , contactInfo.profileInfo.uri.c_str() /* celldata */,
                                3 /* col # */ , contactInfo.registeredName.c_str() /* celldata */,
                                4 /* col # */ , contactInfo.profileInfo.avatar.c_str() /* celldata */,
                                5 /* col # */ , lastMessage.c_str() /* celldata */,
                                -1 /* end */);
            g_free(ringId);
            return;
        }
        g_free(ringId);
        idx++;
    }
}

305 306 307 308
static GtkTreeModel*
create_and_fill_model(ConversationsView *self)
{
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
309 310
    auto store = gtk_list_store_new (6 /* # of cols */ ,
                                     G_TYPE_STRING,
311 312 313 314 315 316 317 318 319
                                     G_TYPE_STRING,
                                     G_TYPE_STRING,
                                     G_TYPE_STRING,
                                     G_TYPE_STRING,
                                     G_TYPE_STRING,
                                     G_TYPE_UINT);
    if(!priv) GTK_TREE_MODEL (store);
    GtkTreeIter iter;

320 321 322 323 324 325
    for (auto conversation : (*priv->accountInfo_)->conversationModel->allFilteredConversations()) {
        if (conversation.participants.empty()) {
            g_debug("Found conversation with empty list of participants - most likely the result of earlier bug.");
            break;
        }

326
        auto contactUri = conversation.participants.front();
327
        try {
328
            auto contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
            auto lastMessage = conversation.interactions.empty() ? "" :
                conversation.interactions.at(conversation.lastMessageUid).body;
            std::replace(lastMessage.begin(), lastMessage.end(), '\n', ' ');
            gtk_list_store_append (store, &iter);
            auto alias = contactInfo.profileInfo.alias;
            alias.erase(std::remove(alias.begin(), alias.end(), '\r'), alias.end());
            gtk_list_store_set (store, &iter,
                                0 /* col # */ , conversation.uid.c_str() /* celldata */,
                                1 /* col # */ , alias.c_str() /* celldata */,
                                2 /* col # */ , contactInfo.profileInfo.uri.c_str() /* celldata */,
                                3 /* col # */ , contactInfo.registeredName.c_str() /* celldata */,
                                4 /* col # */ , contactInfo.profileInfo.avatar.c_str() /* celldata */,
                                5 /* col # */ , lastMessage.c_str() /* celldata */,
                                -1 /* end */);
        } catch (const std::out_of_range&) {
            // ContactModel::getContact() exception
        }
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    }

    return GTK_TREE_MODEL (store);
}

static void
call_conversation(GtkTreeView *self,
                  GtkTreePath *path,
                  G_GNUC_UNUSED GtkTreeViewColumn *column,
                  G_GNUC_UNUSED gpointer user_data)
{
    auto row = std::atoi(gtk_tree_path_to_string(path));
    if (row == -1) return;
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    if (!priv) return;
361 362
    auto conversation = (*priv->accountInfo_)->conversationModel->filteredConversation(row);
    (*priv->accountInfo_)->conversationModel->placeCall(conversation.uid);
363 364 365
}

static void
366
refresh_popup_menu(ConversationsView *self)
367 368 369 370 371 372 373 374
{
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    if (priv->popupMenu_) {
        // Because popup menu is not up to date, we need to update it.
        auto isVisible = gtk_widget_get_visible(priv->popupMenu_);
        // Destroy the not up to date menu.
        gtk_widget_hide(priv->popupMenu_);
        gtk_widget_destroy(priv->popupMenu_);
375
        priv->popupMenu_ = conversation_popup_menu_new(GTK_TREE_VIEW(self), *priv->accountInfo_);
376 377 378 379 380 381
        auto children = gtk_container_get_children (GTK_CONTAINER(priv->popupMenu_));
        auto nbItems = g_list_length(children);
        // Show the new popupMenu_ should be visible
        if (isVisible && nbItems > 0)
            gtk_menu_popup(GTK_MENU(priv->popupMenu_), nullptr, nullptr, nullptr, nullptr, 0, gtk_get_current_event_time());
    }
382 383 384 385 386 387 388
}

static void
select_conversation(GtkTreeSelection *selection, ConversationsView *self)
{
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    refresh_popup_menu(self);
389 390 391 392 393 394 395 396 397
    GtkTreeIter iter;
    GtkTreeModel *model = nullptr;
    gchar *conversationUid = nullptr;

    if (!gtk_tree_selection_get_selected(selection, &model, &iter)) return;

    gtk_tree_model_get(model, &iter,
                       0, &conversationUid,
                       -1);
398
    (*priv->accountInfo_)->conversationModel->selectConversation(std::string(conversationUid));
399 400 401
}

static void
402
conversations_view_init(G_GNUC_UNUSED ConversationsView *self)
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
{
    // Nothing to do
}

static void
show_popup_menu(ConversationsView *self, GdkEventButton *event)
{
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    auto children = gtk_container_get_children (GTK_CONTAINER(priv->popupMenu_));
    auto nbItems = g_list_length(children);
    // Show the new popupMenu_ should be visible
    if (nbItems > 0)
        conversation_popup_menu_show(CONVERSATION_POPUP_MENU(priv->popupMenu_), event);
}

static void
on_drag_data_get(GtkWidget        *treeview,
                 G_GNUC_UNUSED GdkDragContext *context,
                 GtkSelectionData *data,
                 G_GNUC_UNUSED guint info,
                 G_GNUC_UNUSED guint time,
                 G_GNUC_UNUSED gpointer user_data)
{
    g_return_if_fail(IS_CONVERSATIONS_VIEW(treeview));

    /* we always drag the selected row */
    auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
    GtkTreeModel *model = NULL;
    GtkTreeIter iter;

    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
        auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);

        gtk_selection_data_set(data,
                               gdk_atom_intern_static_string(CALL_TARGET),
                               8, /* bytes */
                               (guchar *)path_str,
                               strlen(path_str) + 1);

        g_free(path_str);
    } else {
        g_warning("drag selection not valid");
    }
}

static gboolean
on_drag_drop(GtkWidget      *treeview,
             GdkDragContext *context,
             gint            x,
             gint            y,
             guint           time,
             G_GNUC_UNUSED gpointer user_data)
{
    g_return_val_if_fail(IS_CONVERSATIONS_VIEW(treeview), FALSE);

    GtkTreePath *path = NULL;
    GtkTreeViewDropPosition drop_pos;

    if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
                                          x, y, &path, &drop_pos)) {

        GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);

        if (target_type != GDK_NONE) {
            g_debug("can drop");
            gtk_drag_get_data(treeview, context, target_type, time);
            return TRUE;
        }

        gtk_tree_path_free(path);
    }

    return FALSE;
}

static gboolean
on_drag_motion(GtkWidget      *treeview,
               GdkDragContext *context,
               gint            x,
               gint            y,
               guint           time,
               G_GNUC_UNUSED gpointer user_data)
{
    g_return_val_if_fail(IS_CONVERSATIONS_VIEW(treeview), FALSE);

    GtkTreePath *path = NULL;
    GtkTreeViewDropPosition drop_pos;

    if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
                                          x, y, &path, &drop_pos)) {
        // we only want to drop on a row, not before or after
        if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
            gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
        } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
            gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
        }
        gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
        return TRUE;
    } else {
        // not a row in the treeview, so we cannot drop
        return FALSE;
    }
}

static void
on_drag_data_received(GtkWidget        *treeview,
                      GdkDragContext   *context,
                      gint              x,
                      gint              y,
                      GtkSelectionData *data,
                      G_GNUC_UNUSED guint info,
                      guint             time,
                      G_GNUC_UNUSED gpointer user_data)
{
    g_return_if_fail(IS_CONVERSATIONS_VIEW(treeview));
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(treeview);

    gboolean success = FALSE;

    /* get the source and destination calls */
    auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
    auto type = gtk_selection_data_get_data_type(data);
    g_debug("data type: %s", gdk_atom_name(type));
    if (path_str_source && strlen(path_str_source) > 0) {
        g_debug("source path: %s", path_str_source);

        /* get the destination path */
530
        GtkTreePath *dest_path = nullptr;
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
        if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
            auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));

            GtkTreeIter source, dest;
            gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
            gtk_tree_model_get_iter(model, &dest, dest_path);

            gchar *conversationUidSrc = nullptr;
            gchar *conversationUidDest = nullptr;

            gtk_tree_model_get(model, &source,
                               0, &conversationUidSrc,
                               -1);
            gtk_tree_model_get(model, &dest,
                               0, &conversationUidDest,
                               -1);

548
            (*priv->accountInfo_)->conversationModel->joinConversations(
549 550 551 552 553
                conversationUidSrc,
                conversationUidDest
            );

            gtk_tree_path_free(dest_path);
554 555 556 557
            g_free(conversationUidSrc);
            g_free(conversationUidDest);

            success = TRUE;
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        }
    }

    gtk_drag_finish(context, success, FALSE, time);
}

static void
build_conversations_view(ConversationsView *self)
{
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);

    auto model = create_and_fill_model(self);
    gtk_tree_view_set_model(GTK_TREE_VIEW(self),
                            GTK_TREE_MODEL(model));

574 575
    gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), false);

576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
    // ringId method column
    auto area = gtk_cell_area_box_new();
    auto column = gtk_tree_view_column_new_with_area(area);

    // render the photo
    auto renderer = gtk_cell_renderer_pixbuf_new();
    gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);

    gtk_tree_view_column_set_cell_data_func(
        column,
        renderer,
        (GtkTreeCellDataFunc)render_contact_photo,
        self,
        NULL);

    // render name and last interaction
    renderer = gtk_cell_renderer_text_new();
    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);

    gtk_tree_view_column_set_cell_data_func(
        column,
        renderer,
        (GtkTreeCellDataFunc)render_name_and_last_interaction,
        self,
        NULL);

    // render time of last interaction and number of unread
    renderer = gtk_cell_renderer_text_new();
    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
    gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);

    gtk_tree_view_column_set_cell_data_func(
        column,
        renderer,
        (GtkTreeCellDataFunc)render_time,
        self,
        NULL);

    gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);

    // This view should be synchronized and redraw at each update.
    priv->modelSortedConnection_ = QObject::connect(
620
    &*(*priv->accountInfo_)->conversationModel,
621 622 623 624 625 626 627
    &lrc::api::ConversationModel::modelSorted,
    [self] () {
        auto model = create_and_fill_model(self);

        gtk_tree_view_set_model(GTK_TREE_VIEW(self),
                                GTK_TREE_MODEL(model));
    });
628
    priv->conversationUpdatedConnection_ = QObject::connect(
629
    &*(*priv->accountInfo_)->conversationModel,
630 631 632 633
    &lrc::api::ConversationModel::conversationUpdated,
    [self] (const std::string& uid) {
        update_conversation(self, uid);
    });
634 635

    priv->filterChangedConnection_ = QObject::connect(
636
    &*(*priv->accountInfo_)->conversationModel,
637 638 639 640 641 642 643 644
    &lrc::api::ConversationModel::filterChanged,
    [self] () {
        auto model = create_and_fill_model(self);

        gtk_tree_view_set_model(GTK_TREE_VIEW(self),
                                GTK_TREE_MODEL(model));
    });

645 646 647
    priv->callChangedConnection_ = QObject::connect(
    &*(*priv->accountInfo_)->callModel,
    &lrc::api::NewCallModel::callStatusChanged,
648
    [self, priv] (const std::string&) {
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
        // retrieve currently selected conversation
        GtkTreeIter iter;
        GtkTreeModel *model = nullptr;
        gchar *conversationUid = nullptr;

        auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
        if (!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
        gtk_tree_model_get(model, &iter, 0, &conversationUid, -1);

        // create updated model
        auto new_model = create_and_fill_model(self);
        gtk_tree_view_set_model(GTK_TREE_VIEW(self), GTK_TREE_MODEL(new_model));

        // make sure conversation remains selected
        conversations_view_select_conversation(self, conversationUid);
    });

666 667 668 669 670 671 672 673
    gtk_widget_show_all(GTK_WIDGET(self));

    auto selectionNew = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
    // One left click to select the conversation
    g_signal_connect(selectionNew, "changed", G_CALLBACK(select_conversation), self);
    // Two clicks to placeCall
    g_signal_connect(self, "row-activated", G_CALLBACK(call_conversation), NULL);

674
    priv->popupMenu_ = conversation_popup_menu_new(GTK_TREE_VIEW(self), *priv->accountInfo_);
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
    // Right click to show actions
    g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(show_popup_menu), self);

    /* drag and drop */
    static GtkTargetEntry targetentries[] = {
        { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
    };

    gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
        GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));

    gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
        targetentries, 1, GDK_ACTION_DEFAULT);

    g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
    g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
    g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
    g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
}

static void
conversations_view_dispose(GObject *object)
{
    auto self = CONVERSATIONS_VIEW(object);
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);

    QObject::disconnect(priv->selection_updated);
    QObject::disconnect(priv->layout_changed);
    QObject::disconnect(priv->modelSortedConnection_);
704
    QObject::disconnect(priv->conversationUpdatedConnection_);
705
    QObject::disconnect(priv->filterChangedConnection_);
706
    QObject::disconnect(priv->callChangedConnection_);
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726

    gtk_widget_destroy(priv->popupMenu_);

    G_OBJECT_CLASS(conversations_view_parent_class)->dispose(object);
}

static void
conversations_view_finalize(GObject *object)
{
    G_OBJECT_CLASS(conversations_view_parent_class)->finalize(object);
}

static void
conversations_view_class_init(ConversationsViewClass *klass)
{
    G_OBJECT_CLASS(klass)->finalize = conversations_view_finalize;
    G_OBJECT_CLASS(klass)->dispose = conversations_view_dispose;
}

GtkWidget *
727
conversations_view_new(AccountInfoPointer const & accountInfo)
728 729 730 731
{
    auto self = CONVERSATIONS_VIEW(g_object_new(CONVERSATIONS_VIEW_TYPE, NULL));
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);

732
    priv->accountInfo_ = &accountInfo;
733

734
    if (*priv->accountInfo_) {
735
        build_conversations_view(self);
736 737 738
    } else {
        g_debug("building conversationsview for inexistant account (just removed ?)");
    }
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766

    return (GtkWidget *)self;
}

/**
 * Select a conversation by uid (used to synchronize the selection)
 * @param self
 * @param uid of the conversation
 */
void
conversations_view_select_conversation(ConversationsView *self, const std::string& uid)
{
    auto idx = 0;
    auto model = gtk_tree_view_get_model (GTK_TREE_VIEW(self));
    auto iterIsCorrect = true;
    GtkTreeIter iter;

    while(iterIsCorrect) {
        iterIsCorrect = gtk_tree_model_iter_nth_child (model, &iter, nullptr, idx);
        if (!iterIsCorrect)
            break;
        gchar *ringId;
        gtk_tree_model_get (model, &iter,
                            0 /* col# */, &ringId /* data */,
                            -1);
        if(std::string(ringId) == uid) {
            auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
            gtk_tree_selection_select_iter(selection, &iter);
767
            refresh_popup_menu(self);
768 769 770 771 772 773 774
            g_free(ringId);
            return;
        }
        g_free(ringId);
        idx++;
    }
}
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793

int
conversations_view_get_current_selected(ConversationsView *self)
{

    g_return_val_if_fail(IS_CONVERSATIONS_VIEW(self), -1);

    /* we always drag the selected row */
    auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
    GtkTreeModel *model = NULL;
    GtkTreeIter iter;

    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
        auto path = gtk_tree_model_get_path(model, &iter);
        auto idx = gtk_tree_path_get_indices(path);
        return idx[0];
    }
    return -1;
}
794 795 796 797 798 799 800 801

void
conversations_view_set_theme(ConversationsView *self, bool darkTheme) {
    g_return_if_fail(IS_CONVERSATIONS_VIEW(self));
    auto priv = CONVERSATIONS_VIEW_GET_PRIVATE(self);
    priv->useDarkTheme = darkTheme;
}