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
}