chatview.cpp 27.3 KB
Newer Older
1
/*
Sébastien Blin's avatar
Sébastien Blin committed
2
 *  Copyright (C) 2016-2019 Savoir-faire Linux Inc.
3
 *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
aviau's avatar
aviau committed
4
 *  Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
5 6
 *  Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>
 *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
7
 *  Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
 */

#include "chatview.h"

26 27
// std
#include <algorithm>
28
#include <fstream>
29

30 31 32
// GTK
#include <glib/gi18n.h>

33 34 35
// Qt
#include <QSize>

36 37 38 39
// LRC
#include <api/contactmodel.h>
#include <api/conversationmodel.h>
#include <api/contact.h>
40 41
#include <api/newcallmodel.h>
#include <api/call.h>
42

43
// Client
44
#include "marshals.h"
45 46 47 48 49
#include "utils/files.h"
#include "native/pixbufmanipulator.h"
/* size of avatar */
static constexpr int AVATAR_WIDTH  = 150; /* px */
static constexpr int AVATAR_HEIGHT = 150; /* px */
50

51 52 53 54 55 56 57 58 59
struct CppImpl {
    struct Interaction {
        std::string conv;
        uint64_t id;
        lrc::api::interaction::Info info;
    };
    std::vector<Interaction> interactionsBuffer_;
};

60 61 62 63 64 65 66 67 68 69 70 71 72 73
struct _ChatView
{
    GtkBox parent;
};

struct _ChatViewClass
{
    GtkBoxClass parent_class;
};

typedef struct _ChatViewPrivate ChatViewPrivate;

struct _ChatViewPrivate
{
aviau's avatar
aviau committed
74 75
    GtkWidget *box_webkit_chat_container;
    GtkWidget *webkit_chat_container;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
76

77 78
    GSettings *settings;

79
    lrc::api::conversation::Info* conversation_;
80
    AccountInfoPointer const * accountInfo_;
81

82
    QMetaObject::Connection new_interaction_connection;
83
    QMetaObject::Connection interaction_removed;
84 85
    QMetaObject::Connection update_interaction_connection;
    QMetaObject::Connection update_add_to_conversations;
86 87

    gulong webkit_ready;
88
    gulong webkit_send_text;
89
    gulong webkit_drag_drop;
90 91 92

    bool ready_ {false};
    CppImpl* cpp_;
93 94 95 96 97 98 99 100
};

G_DEFINE_TYPE_WITH_PRIVATE(ChatView, chat_view, GTK_TYPE_BOX);

#define CHAT_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CHAT_VIEW_TYPE, ChatViewPrivate))

enum {
    NEW_MESSAGES_DISPLAYED,
101
    HIDE_VIEW_CLICKED,
102 103
    PLACE_CALL_CLICKED,
    PLACE_AUDIO_CALL_CLICKED,
104 105
    ADD_CONVERSATION_CLICKED,
    SEND_TEXT_CLICKED,
106 107 108 109 110 111 112 113 114 115 116 117 118 119
    LAST_SIGNAL
};

static guint chat_view_signals[LAST_SIGNAL] = { 0 };

static void
chat_view_dispose(GObject *object)
{
    ChatView *view;
    ChatViewPrivate *priv;

    view = CHAT_VIEW(object);
    priv = CHAT_VIEW_GET_PRIVATE(view);

120 121
    QObject::disconnect(priv->new_interaction_connection);
    QObject::disconnect(priv->update_interaction_connection);
122
    QObject::disconnect(priv->interaction_removed);
123
    QObject::disconnect(priv->update_add_to_conversations);
aviau's avatar
aviau committed
124 125 126 127

    /* Destroying the box will also destroy its children, and we wouldn't
     * want that. So we remove the webkit_chat_container from the box. */
    if (priv->webkit_chat_container) {
128 129 130
        /* disconnect for webkit signals */
        g_signal_handler_disconnect(priv->webkit_chat_container, priv->webkit_ready);
        priv->webkit_ready = 0;
131 132
        g_signal_handler_disconnect(priv->webkit_chat_container, priv->webkit_send_text);
        priv->webkit_send_text = 0;
133 134
        g_signal_handler_disconnect(priv->webkit_chat_container, priv->webkit_drag_drop);
        priv->webkit_drag_drop = 0;
135

aviau's avatar
aviau committed
136 137 138 139 140 141
        gtk_container_remove(
            GTK_CONTAINER(priv->box_webkit_chat_container),
            GTK_WIDGET(priv->webkit_chat_container)
        );
        priv->webkit_chat_container = nullptr;
    }
142 143 144 145

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

146 147 148 149 150 151
static void
hide_chat_view(G_GNUC_UNUSED GtkWidget *widget, ChatView *self)
{
    g_signal_emit(G_OBJECT(self), chat_view_signals[HIDE_VIEW_CLICKED], 0);
}

152 153 154 155 156 157 158 159 160 161 162 163
static void
display_links_toggled(ChatView *self)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
    if (priv->webkit_chat_container) {
        webkit_chat_container_set_display_links(
            WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
            g_settings_get_boolean(priv->settings, "enable-display-links")
        );
    }
}

164 165 166 167
static void
placecall_clicked(ChatView *self)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
168
    if (!priv->conversation_) return;
169
    g_signal_emit(G_OBJECT(self), chat_view_signals[PLACE_CALL_CLICKED], 0, priv->conversation_->uid.c_str());
170 171
}

172 173 174 175
static void
place_audio_call_clicked(ChatView *self)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
176 177
    if (!priv->conversation_) return;
    g_signal_emit(G_OBJECT(self), chat_view_signals[PLACE_AUDIO_CALL_CLICKED], 0, priv->conversation_->uid.c_str());
178 179
}

180
static void
181
add_to_conversations_clicked(ChatView *self)
182 183
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
184
    if (!priv->conversation_) return;
185 186 187 188 189 190 191 192 193
    g_signal_emit(G_OBJECT(self), chat_view_signals[ADD_CONVERSATION_CLICKED], 0, priv->conversation_->uid.c_str());
}

static void
send_text_clicked(ChatView *self, const std::string& body)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
    if (!priv->conversation_) return;
    g_signal_emit(G_OBJECT(self), chat_view_signals[SEND_TEXT_CLICKED], 0, priv->conversation_->uid.c_str(), body.c_str());
194 195
}

196
static gchar*
197
file_to_manipulate(GtkWindow* top_window, bool send)
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
{
    GtkWidget* dialog;
    GtkFileChooserAction action = send? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE;
    gint res;
    gchar* filename = nullptr;

    dialog = gtk_file_chooser_dialog_new(send? _("Send File") : _("Save File"),
                                         top_window,
                                         action,
                                         _("_Cancel"),
                                         GTK_RESPONSE_CANCEL,
                                         send? _("_Open"): _("_Save"),
                                         GTK_RESPONSE_ACCEPT,
                                         nullptr);

    res = gtk_dialog_run (GTK_DIALOG(dialog));

    if (res == GTK_RESPONSE_ACCEPT) {
        auto chooser = GTK_FILE_CHOOSER(dialog);
        filename = gtk_file_chooser_get_filename(chooser);
    }
    gtk_widget_destroy (dialog);

    return filename;
}

Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
224 225
static void update_chatview_frame(ChatView *self);

226
static void
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
227
webkit_chat_container_script_dialog(GtkWidget* webview, gchar *interaction, ChatView* self)
228
{
229 230
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
    auto order = std::string(interaction);
231
    if (!priv->conversation_) return;
232
    if (order == "ACCEPT") {
233
        (*priv->accountInfo_)->conversationModel->makePermanent(priv->conversation_->uid);
234
    } else if (order == "REFUSE") {
235
        (*priv->accountInfo_)->conversationModel->removeConversation(priv->conversation_->uid);
236
    } else if (order == "BLOCK") {
237
        (*priv->accountInfo_)->conversationModel->removeConversation(priv->conversation_->uid, true);
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
238 239 240 241 242
    } else if (order == "UNBLOCK") {
        try {
            auto contactUri = priv->conversation_->participants.front();
            auto& contact = (*priv->accountInfo_)->contactModel->getContact(contactUri);
            (*priv->accountInfo_)->contactModel->addContact(contact);
243
        } catch (std::out_of_range&) {
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
244 245 246 247 248 249 250 251
            g_debug("webkit_chat_container_script_dialog: oor while retrieving invalid contact info. Chatview bug ?");
        }
    } else if (order.find("PLACE_CALL") == 0) {
        placecall_clicked(self);
    } else if (order.find("PLACE_AUDIO_CALL") == 0) {
        place_audio_call_clicked(self);
    } else if (order.find("CLOSE_CHATVIEW") == 0) {
        hide_chat_view(webview, self);
252 253
    } else if (order.find("SEND:") == 0) {
        auto toSend = order.substr(std::string("SEND:").size());
254 255 256 257 258 259
        if ((*priv->accountInfo_)->profileInfo.type == lrc::api::profile::Type::RING) {
            (*priv->accountInfo_)->conversationModel->sendMessage(priv->conversation_->uid, toSend);
        } else {
            // For SIP accounts, we need to wait that the conversation is created to send text
            send_text_clicked(self, toSend);
        }
260
    } else if (order.find("SEND_FILE") == 0) {
261
        if (auto model = (*priv->accountInfo_)->conversationModel.get()) {
262 263 264 265
            if (auto filename = file_to_manipulate(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))), true))
                model->sendFile(priv->conversation_->uid, filename, g_path_get_basename(filename));
        }
    } else if (order.find("ACCEPT_FILE:") == 0) {
266
        if (auto model = (*priv->accountInfo_)->conversationModel.get()) {
267 268
            try {
                auto interactionId = std::stoull(order.substr(std::string("ACCEPT_FILE:").size()));
269 270

                lrc::api::datatransfer::Info info = {};
271
                (*priv->accountInfo_)->conversationModel->getTransferInfo(interactionId, info);
272

Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
273
                // get preferred directory destination.
274 275 276 277 278 279 280 281 282 283 284
                auto* download_directory_variant = g_settings_get_value(priv->settings, "download-folder");
                char* download_directory_value;
                g_variant_get(download_directory_variant, "&s", &download_directory_value);
                std::string default_download_dir = g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD);
                auto current_value = std::string(download_directory_value);
                if (current_value.empty()) {
                    g_settings_set_value(priv->settings, "download-folder", g_variant_new("s", default_download_dir.c_str()));
                }
                // get full path
                std::string filename = current_value.empty()? default_download_dir.c_str() : download_directory_value;
                if (!filename.empty() && filename.back() != '/') filename += "/";
285 286 287 288
                auto wantedFilename = filename + info.displayName;
                auto duplicate = 0;
                while (std::ifstream(wantedFilename).good()) {
                    ++duplicate;
289 290 291 292 293
                    auto extensionIdx = info.displayName.find_last_of(".");
                    if (extensionIdx == std::string::npos)
                        wantedFilename = filename + info.displayName + " (" + std::to_string(duplicate) + ")";
                    else
                        wantedFilename = filename + info.displayName.substr(0, extensionIdx) + " (" + std::to_string(duplicate) + ")" + info.displayName.substr(extensionIdx);
294 295
                }
                model->acceptTransfer(priv->conversation_->uid, interactionId, wantedFilename);
296 297 298 299 300
            } catch (...) {
                // ignore
            }
        }
    } else if (order.find("REFUSE_FILE:") == 0) {
301
        if (auto model = (*priv->accountInfo_)->conversationModel.get()) {
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
            try {
                auto interactionId = std::stoull(order.substr(std::string("REFUSE_FILE:").size()));
                model->cancelTransfer(priv->conversation_->uid, interactionId);
            } catch (...) {
                // ignore
            }
        }
    } else if (order.find("OPEN_FILE:") == 0) {
        // Get text body
        auto filename {"file://" + order.substr(std::string("OPEN_FILE:").size())};
        filename.erase(std::find_if(filename.rbegin(), filename.rend(),
            std::not1(std::ptr_fun<int, int>(std::isspace))).base(), filename.end());
        GError* error = nullptr;
        if (!gtk_show_uri(nullptr, filename.c_str(), GDK_CURRENT_TIME, &error)) {
            g_debug("Could not open file: %s", error->message);
            g_error_free(error);
        }
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
319
    } else if (order.find("ADD_TO_CONVERSATIONS") == 0) {
320
        add_to_conversations_clicked(self);
321 322 323 324 325 326 327 328
    } else if (order.find("DELETE_INTERACTION:") == 0) {
        try {
            auto interactionId = std::stoull(order.substr(std::string("DELETE_INTERACTION:").size()));
            if (!priv->conversation_) return;
            (*priv->accountInfo_)->conversationModel->clearInteractionFromConversation(priv->conversation_->uid, interactionId);
        } catch (...) {
            g_warning("delete interaction failed: can't find %s", order.substr(std::string("DELETE_INTERACTION:").size()).c_str());
        }
329 330 331 332 333 334 335 336
    } else if (order.find("RETRY_INTERACTION:") == 0) {
        try {
            auto interactionId = std::stoull(order.substr(std::string("RETRY_INTERACTION:").size()));
            if (!priv->conversation_) return;
            (*priv->accountInfo_)->conversationModel->retryInteraction(priv->conversation_->uid, interactionId);
        } catch (...) {
            g_warning("delete interaction failed: can't find %s", order.substr(std::string("RETRY_INTERACTION:").size()).c_str());
        }
337 338 339
    }
}

340 341 342 343 344 345
static void
chat_view_init(ChatView *view)
{
    gtk_widget_init_template(GTK_WIDGET(view));

    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(view);
346
    priv->settings = g_settings_new_full(get_settings_schema(), NULL, NULL);
347 348 349 350 351 352 353 354
}

static void
chat_view_class_init(ChatViewClass *klass)
{
    G_OBJECT_CLASS(klass)->dispose = chat_view_dispose;

    gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
355
                                                "/net/jami/JamiGnome/chatview.ui");
356

aviau's avatar
aviau committed
357
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, box_webkit_chat_container);
358 359

    chat_view_signals[NEW_MESSAGES_DISPLAYED] = g_signal_new (
360
        "new-interactions-displayed",
361 362 363 364 365 366 367
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);
368 369 370 371 372 373 374 375 376 377

    chat_view_signals[HIDE_VIEW_CLICKED] = g_signal_new (
        "hide-view-clicked",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397

    chat_view_signals[PLACE_CALL_CLICKED] = g_signal_new (
        "place-call-clicked",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

    chat_view_signals[PLACE_AUDIO_CALL_CLICKED] = g_signal_new (
        "place-audio-call-clicked",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417

    chat_view_signals[ADD_CONVERSATION_CLICKED] = g_signal_new (
        "add-conversation-clicked",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
        0,
        nullptr,
        nullptr,
        g_cclosure_marshal_VOID__STRING,
        G_TYPE_NONE, 1, G_TYPE_STRING);

    chat_view_signals[SEND_TEXT_CLICKED] = g_signal_new (
        "send-text-clicked",
        G_TYPE_FROM_CLASS(klass),
        (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
        0,
        nullptr,
        nullptr,
        g_cclosure_user_marshal_VOID__STRING_STRING,
        G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
418 419
}

aviau's avatar
aviau committed
420
static void
421
print_interaction_to_buffer(ChatView* self, uint64_t interactionId, const lrc::api::interaction::Info& interaction)
aviau's avatar
aviau committed
422 423 424
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

425
    if (!priv->conversation_) return;
426
    if (!interaction.isRead)
427
        (*priv->accountInfo_)->conversationModel->setInteractionRead(priv->conversation_->uid, interactionId);
428 429

    webkit_chat_container_print_new_interaction(
aviau's avatar
aviau committed
430
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
431
        *(*priv->accountInfo_)->conversationModel,
432 433
        interactionId,
        interaction
aviau's avatar
aviau committed
434
    );
435 436
}

437
static void
438
update_interaction(ChatView* self, uint64_t interactionId, const lrc::api::interaction::Info& interaction)
439 440
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
441 442
    webkit_chat_container_update_interaction(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
443
        *(*priv->accountInfo_)->conversationModel,
444 445
        interactionId,
        interaction
446
    );
447
}
448

449 450 451 452 453 454 455 456 457 458
static void
remove_interaction(ChatView* self, uint64_t interactionId)
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
    webkit_chat_container_remove_interaction(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
        interactionId
    );
}

459 460 461 462 463
static void
load_participants_images(ChatView *self)
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
464

465
    // Contact
466
    if (!priv->conversation_) return;
467
    auto contactUri = priv->conversation_->participants.front();
468
    try{
469
        auto& contact = (*priv->accountInfo_)->contactModel->getContact(contactUri);
470 471 472 473 474 475 476 477 478 479 480
        std::string avatar_str = contact.profileInfo.avatar;
        if (avatar_str.empty()) {
            auto var_photo = Interfaces::PixbufManipulator().conversationPhoto(
                *priv->conversation_,
                **(priv->accountInfo_),
                QSize(AVATAR_WIDTH, AVATAR_HEIGHT),
                contact.isPresent
            );
            auto image = var_photo.value<std::shared_ptr<GdkPixbuf>>();
            auto ba = Interfaces::PixbufManipulator().toByteArray(var_photo);
            avatar_str = ba.toBase64().toStdString();
481
        }
482 483 484 485 486
        webkit_chat_container_set_sender_image(
            WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
            contactUri,
            avatar_str
        );
487 488
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
489 490
    }

491
    // For this account
492 493 494 495 496 497 498
    std::string avatar_str = (*priv->accountInfo_)->profileInfo.avatar;
    if (avatar_str.empty()) {
        auto default_photo = QVariant::fromValue(Interfaces::PixbufManipulator().scaleAndFrame(
            Interfaces::PixbufManipulator().generateAvatar("", "").get(),
            QSize(AVATAR_WIDTH, AVATAR_HEIGHT), false));
        auto ba = Interfaces::PixbufManipulator().toByteArray(default_photo);
        avatar_str = ba.toBase64().toStdString();
499
    }
500 501 502 503 504
    webkit_chat_container_set_sender_image(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
        (*priv->accountInfo_)->profileInfo.uri,
        avatar_str
    );
505 506
}

507
static void
508
print_text_recording(ChatView *self)
509 510 511 512
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

513
    // Read interactions
514
    if (!priv->conversation_) return;
515
    for (const auto& it: priv->conversation_->interactions) {
516
        if (!it.second.isRead)
517
            (*priv->accountInfo_)->conversationModel->setInteractionRead(priv->conversation_->uid, it.first);
518 519
    }

520 521
    webkit_chat_container_print_history(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
522
        *(*priv->accountInfo_)->conversationModel,
523 524
        priv->conversation_->interactions
    );
525 526
}

aviau's avatar
aviau committed
527 528 529 530 531 532 533 534 535 536 537 538
static void
webkit_chat_container_ready(ChatView* self)
{
    /* The webkit chat container has loaded the javascript libraries, we can
     * now use it. */

    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

    webkit_chat_container_clear(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container)
    );

539
    display_links_toggled(self);
540 541 542
    print_text_recording(self);
    load_participants_images(self);

543 544 545 546
    priv->ready_ = true;
    for (const auto& interaction: priv->cpp_->interactionsBuffer_) {
        if (interaction.conv == priv->conversation_->uid) {
            print_interaction_to_buffer(self, interaction.id, interaction.info);
547
        }
548
    }
549

550
    priv->update_interaction_connection = QObject::connect(
551
    &*(*priv->accountInfo_)->conversationModel, &lrc::api::ConversationModel::interactionStatusUpdated,
552
    [self, priv](const std::string& uid, uint64_t msgId, lrc::api::interaction::Info msg) {
553
        if (!priv->conversation_) return;
554
        if (uid == priv->conversation_->uid) {
555 556 557 558
            update_interaction(self, msgId, msg);
        }
    });

559 560 561 562 563 564 565 566 567
    priv->interaction_removed = QObject::connect(
    &*(*priv->accountInfo_)->conversationModel, &lrc::api::ConversationModel::interactionRemoved,
    [self, priv](const std::string& convUid, uint64_t interactionId) {
        if (!priv->conversation_) return;
        if (convUid == priv->conversation_->uid) {
            remove_interaction(self, interactionId);
        }
    });

568
    if (!priv->conversation_) return;
569
    auto contactUri = priv->conversation_->participants.front();
570
    try {
571
        auto contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);
572 573 574 575 576 577 578 579 580
        auto bestName = contactInfo.profileInfo.alias;
        if (bestName.empty())
            bestName = contactInfo.registeredName;
        if (bestName.empty())
            bestName = contactInfo.profileInfo.uri;
        bestName.erase(std::remove(bestName.begin(), bestName.end(), '\r'), bestName.end());
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
    }
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
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

    update_chatview_frame(self);
}

static void
update_chatview_frame(ChatView* self)
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
    if (!priv->conversation_) return;

    auto contactUri = priv->conversation_->participants.front();

    lrc::api::contact::Info contactInfo;
    try {
        contactInfo = (*priv->accountInfo_)->contactModel->getContact(contactUri);
    } catch (const std::out_of_range&) {
        g_debug("update_chatview_frame: failed to retrieve contactInfo");
        return;
    }

    // get alias and bestName
    auto alias = contactInfo.profileInfo.alias;
    auto bestName = contactInfo.registeredName;
    if (bestName.empty())
        bestName = contactInfo.profileInfo.uri;
607 608
    if (bestName == alias)
        alias = "";
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
609 610 611 612 613 614
    bestName.erase(std::remove(bestName.begin(), bestName.end(), '\r'), bestName.end());
    alias.erase(std::remove(alias.begin(), alias.end(), '\r'), alias.end());

    // get temporary status
    bool temp = contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY || contactInfo.profileInfo.type == lrc::api::profile::Type::PENDING;

615 616 617
    webkit_chat_update_chatview_frame(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                                     (*priv->accountInfo_)->enabled,
                                     contactInfo.isBanned, temp, alias.c_str(), bestName.c_str());
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
618 619 620

    webkit_chat_container_set_invitation(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                                             (contactInfo.profileInfo.type == lrc::api::profile::Type::PENDING),
Sébastien Blin's avatar
Sébastien Blin committed
621
                                             bestName,
622
                                             contactInfo.profileInfo.uri);
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644

    // hide navbar if we are in call
    try {
        std::string callId;
        if (priv->conversation_->confId.empty()) {
            callId = priv->conversation_->callId;
        } else {
            callId = priv->conversation_->confId;
        }

        if (*priv->accountInfo_) {
            const lrc::api::call::Status& status = (*priv->accountInfo_)->callModel->getCall(callId).status;
            if (status != lrc::api::call::Status::ENDED &&
                status != lrc::api::call::Status::INVALID &&
                status != lrc::api::call::Status::TERMINATING) {
                g_debug("call has status %s, hiding", lrc::api::call::to_string(status).c_str());
                chat_view_set_header_visible(self, FALSE);
            } else {
                chat_view_set_header_visible(self, TRUE);
            }
        }
    } catch (const std::out_of_range&) {}
aviau's avatar
aviau committed
645 646
}

647 648 649 650 651 652 653 654
static void
on_webkit_drag_drop(GtkWidget*, gchar* data, ChatView* self)
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
    if (!priv->conversation_) return;
    if (!data) return;

655
    GError *error = nullptr;
656
    auto* filename_uri = g_filename_from_uri(data, nullptr, &error);
657 658 659 660 661
    if (error) {
        g_warning("Unable to exec g_filename_from_uri on %s", data);
        g_error_free(error);
        return;
    }
662 663 664
    std::string data_str = filename_uri;
    g_free(filename_uri);

665 666 667
    // Only take files
    if (data_str.find("\r\n") == std::string::npos) return;
    const auto LEN_END = std::string("\r\n").length();
668 669 670 671 672 673
    if (data_str.length() > LEN_END) {
        // remove \r\n from the string
        data_str = data_str.substr(0, data_str.length() - LEN_END);
    } else {
        // Nothing valid to drop, abort.
        return;
674 675 676 677 678 679 680
    }

    if (auto model = (*priv->accountInfo_)->conversationModel.get()) {
        model->sendFile(priv->conversation_->uid, data_str, g_path_get_basename(data_str.c_str()));
    }
}

aviau's avatar
aviau committed
681 682 683 684 685 686 687 688
static void
build_chat_view(ChatView* self)
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

    gtk_container_add(GTK_CONTAINER(priv->box_webkit_chat_container), priv->webkit_chat_container);
    gtk_widget_show(priv->webkit_chat_container);

Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
689
    update_chatview_frame(self);
aviau's avatar
aviau committed
690

691
    priv->webkit_ready = g_signal_connect_swapped(
aviau's avatar
aviau committed
692 693 694 695 696 697
        priv->webkit_chat_container,
        "ready",
        G_CALLBACK(webkit_chat_container_ready),
        self
    );

698
    priv->webkit_send_text = g_signal_connect(priv->webkit_chat_container,
699 700
        "script-dialog",
        G_CALLBACK(webkit_chat_container_script_dialog),
701 702
        self);

703 704 705 706 707 708 709
    priv->webkit_drag_drop = g_signal_connect(
        priv->webkit_chat_container,
        "data-dropped",
        G_CALLBACK(on_webkit_drag_drop),
        self
    );

710 711 712 713 714 715 716 717 718 719 720 721 722 723
    priv->new_interaction_connection = QObject::connect(
    &*(*priv->accountInfo_)->conversationModel, &lrc::api::ConversationModel::newInteraction,
    [self, priv](const std::string& uid, uint64_t interactionId, lrc::api::interaction::Info interaction) {
        if (!priv->conversation_) return;
        if (!priv->ready_ && priv->cpp_) {
            priv->cpp_->interactionsBuffer_.emplace_back(CppImpl::Interaction {
                uid, interactionId, interaction});
        } else if (uid == priv->conversation_->uid) {
            print_interaction_to_buffer(self, interactionId, interaction);
        }
    });

    priv->cpp_ = new CppImpl();

aviau's avatar
aviau committed
724 725
    if (webkit_chat_container_is_ready(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container)))
        webkit_chat_container_ready(self);
726
}
Stepan Salenikovich's avatar
Stepan Salenikovich committed
727 728

GtkWidget *
729
chat_view_new (WebKitChatContainer* webkit_chat_container,
730
               AccountInfoPointer const & accountInfo,
731
               lrc::api::conversation::Info* conversation)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
732 733 734
{
    ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));

aviau's avatar
aviau committed
735 736
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
    priv->webkit_chat_container = GTK_WIDGET(webkit_chat_container);
737
    priv->conversation_ = conversation;
738
    priv->accountInfo_ = &accountInfo;
aviau's avatar
aviau committed
739 740

    build_chat_view(self);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
741 742 743
    return (GtkWidget *)self;
}

744
void
745
chat_view_update_temporary(ChatView* self)
746
{
747
    g_return_if_fail(IS_CHAT_VIEW(self));
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
748
    update_chatview_frame(self);
749 750
}

751 752
lrc::api::conversation::Info
chat_view_get_conversation(ChatView *self)
753
{
754
    g_return_val_if_fail(IS_CHAT_VIEW(self), lrc::api::conversation::Info());
755
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
756
    return *priv->conversation_;
757
}
758 759 760 761 762

void
chat_view_set_header_visible(ChatView *self, gboolean visible)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
763
    webkit_chat_set_header_visible(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container), visible);
764
}