chatview.cpp 18.2 KB
Newer Older
1
/*
Guillaume Roguez's avatar
Guillaume Roguez committed
2
 *  Copyright (C) 2016-2018 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>
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 *
 *  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"

25
26
// std
#include <algorithm>
27

28
29
30
31
// LRC
#include <api/contactmodel.h>
#include <api/conversationmodel.h>
#include <api/contact.h>
32

33
34
// Client
#include "utils/files.h"
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct _ChatView
{
    GtkBox parent;
};

struct _ChatViewClass
{
    GtkBoxClass parent_class;
};

typedef struct _ChatViewPrivate ChatViewPrivate;

struct _ChatViewPrivate
{
aviau's avatar
aviau committed
50
51
    GtkWidget *box_webkit_chat_container;
    GtkWidget *webkit_chat_container;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
52
53
    GtkWidget *hbox_chat_info;
    GtkWidget *label_peer;
54
    GtkWidget *label_cm;
55
    GtkWidget *button_close_chatview;
56
    GtkWidget *button_placecall;
57
    GtkWidget *button_add_to_conversations;
58
    GtkWidget *button_place_audio_call;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
59

60
61
    GSettings *settings;

62
63
64
    lrc::api::conversation::Info* conversation_;
    AccountContainer* accountContainer_;
    bool isTemporary_;
65

66
67
68
    QMetaObject::Connection new_interaction_connection;
    QMetaObject::Connection update_interaction_connection;
    QMetaObject::Connection update_add_to_conversations;
69
70

    gulong webkit_ready;
71
    gulong webkit_send_text;
72
73
74
75
76
77
78
79
};

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,
80
    HIDE_VIEW_CLICKED,
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    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);

95
96
97
    QObject::disconnect(priv->new_interaction_connection);
    QObject::disconnect(priv->update_interaction_connection);
    QObject::disconnect(priv->update_add_to_conversations);
aviau's avatar
aviau committed
98
99
100
101

    /* 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) {
102
103
104
        /* disconnect for webkit signals */
        g_signal_handler_disconnect(priv->webkit_chat_container, priv->webkit_ready);
        priv->webkit_ready = 0;
105
106
        g_signal_handler_disconnect(priv->webkit_chat_container, priv->webkit_send_text);
        priv->webkit_send_text = 0;
107

aviau's avatar
aviau committed
108
109
110
111
112
113
        gtk_container_remove(
            GTK_CONTAINER(priv->box_webkit_chat_container),
            GTK_WIDGET(priv->webkit_chat_container)
        );
        priv->webkit_chat_container = nullptr;
    }
114
115
116
117

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

118
119
120
121
122
123
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);
}

124
125
126
127
128
129
130
131
132
133
134
135
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")
        );
    }
}

136
137
138
139
static void
placecall_clicked(ChatView *self)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
140
    if (!priv->conversation_) return;
141
    priv->accountContainer_->info.conversationModel->placeCall(priv->conversation_->uid);
142
143
}

144
145
146
147
148
149
150
151
152
153
154
static void
place_audio_call_clicked(ChatView *self)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);

    if (!priv->conversation_)
        return;

    priv->accountContainer_->info.conversationModel->placeAudioOnlyCall(priv->conversation_->uid);
}

155
static void
156
button_add_to_conversations_clicked(ChatView *self)
157
158
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
159
    if (!priv->conversation_) return;
160
    priv->accountContainer_->info.conversationModel->makePermanent(priv->conversation_->uid);
161
162
}

163
static void
164
webkit_chat_container_script_dialog(G_GNUC_UNUSED GtkWidget* webview, gchar *interaction, ChatView* self)
165
{
166
167
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
    auto order = std::string(interaction);
168
    if (!priv->conversation_) return;
169
170
171
172
173
174
175
176
177
178
    if (order == "ACCEPT") {
        priv->accountContainer_->info.conversationModel->makePermanent(priv->conversation_->uid);
    } else if (order == "REFUSE") {
        priv->accountContainer_->info.conversationModel->removeConversation(priv->conversation_->uid);
    } else if (order == "BLOCK") {
        priv->accountContainer_->info.conversationModel->removeConversation(priv->conversation_->uid, true);
    } else if (order.find("SEND:") == 0) {
        // Get text body
        auto toSend = order.substr(std::string("SEND:").size());
        priv->accountContainer_->info.conversationModel->sendMessage(priv->conversation_->uid, toSend);
179
180
181
    }
}

182
183
184
185
186
187
static void
chat_view_init(ChatView *view)
{
    gtk_widget_init_template(GTK_WIDGET(view));

    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(view);
188
    priv->settings = g_settings_new_full(get_ring_schema(), NULL, NULL);
189

190
    g_signal_connect(priv->button_close_chatview, "clicked", G_CALLBACK(hide_chat_view), view);
191
    g_signal_connect_swapped(priv->button_placecall, "clicked", G_CALLBACK(placecall_clicked), view);
192
    g_signal_connect_swapped(priv->button_add_to_conversations, "clicked", G_CALLBACK(button_add_to_conversations_clicked), view);
193
    g_signal_connect_swapped(priv->button_place_audio_call, "clicked", G_CALLBACK(place_audio_call_clicked), view);
194
195
196
197
198
199
200
201
202
203
}

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),
                                                "/cx/ring/RingGnome/chatview.ui");

aviau's avatar
aviau committed
204
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, box_webkit_chat_container);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
205
206
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, hbox_chat_info);
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, label_peer);
207
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, label_cm);
208
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_close_chatview);
209
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_placecall);
210
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_add_to_conversations);
211
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_place_audio_call);
212
213

    chat_view_signals[NEW_MESSAGES_DISPLAYED] = g_signal_new (
214
        "new-interactions-displayed",
215
216
217
218
219
220
221
        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);
222
223
224
225
226
227
228
229
230
231

    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);
232
233
}

aviau's avatar
aviau committed
234
static void
235
print_interaction_to_buffer(ChatView* self, uint64_t interactionId, const lrc::api::interaction::Info& interaction)
aviau's avatar
aviau committed
236
237
238
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

239
    if (!priv->conversation_) return;
240
241
242
243
    if (interaction.status == lrc::api::interaction::Status::UNREAD)
        priv->accountContainer_->info.conversationModel->setInteractionRead(priv->conversation_->uid, interactionId);

    webkit_chat_container_print_new_interaction(
aviau's avatar
aviau committed
244
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
245
246
        interactionId,
        interaction
aviau's avatar
aviau committed
247
    );
248
249
}

250
static void
251
update_interaction(ChatView* self, uint64_t interactionId, const lrc::api::interaction::Info& interaction)
252
253
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
254
255
256
257
    webkit_chat_container_update_interaction(
        WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
        interactionId,
        interaction
258
    );
259
}
260

261
262
263
264
265
static void
load_participants_images(ChatView *self)
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
266

267
    // Contact
268
    if (!priv->conversation_) return;
269
    auto contactUri = priv->conversation_->participants.front();
270
271
272
273
274
275
276
277
278
279
280
    try{
        auto& contact = priv->accountContainer_->info.contactModel->getContact(contactUri);
        if (!contact.profileInfo.avatar.empty()) {
            webkit_chat_container_set_sender_image(
                WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                priv->accountContainer_->info.contactModel->getContactProfileId(contactUri),
                contact.profileInfo.avatar
                );
        }
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
281
282
    }

283
284
285
286
287
288
289
    // For this account
    if (!priv->accountContainer_->info.profileInfo.avatar.empty()) {
        webkit_chat_container_set_sender_image(
            WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
            priv->accountContainer_->info.contactModel->getContactProfileId(priv->accountContainer_->info.profileInfo.uri),
            priv->accountContainer_->info.profileInfo.avatar
        );
290
291
292
    }
}

293
static void
294
print_text_recording(ChatView *self)
295
296
297
298
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

299
    if (!priv->conversation_) return;
300
301
    for (const auto& interaction : priv->conversation_->interactions)
        print_interaction_to_buffer(self, interaction.first, interaction.second);
302

303
    QObject::disconnect(priv->new_interaction_connection);
304
305
}

306
static void
307
update_add_to_conversations(ChatView *self)
308
309
310
{
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);

311
    if (!priv->conversation_) return;
312
    auto participant = priv->conversation_->participants[0];
313
314
315
316
317
318
319
320
    try {
        auto contactInfo = priv->accountContainer_->info.contactModel->getContact(participant);
        if(contactInfo.profileInfo.type != lrc::api::profile::Type::TEMPORARY
           && contactInfo.profileInfo.type != lrc::api::profile::Type::PENDING)
            gtk_widget_hide(priv->button_add_to_conversations);
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
    }
321
322
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
323
324
325
326
327
static void
update_contact_methods(ChatView *self)
{
    g_return_if_fail(IS_CHAT_VIEW(self));
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
328
    if (!priv->conversation_) return;
329
    auto contactUri = priv->conversation_->participants.front();
330
331
332
333
334
335
336
337
338
339
340
    try {
        auto contactInfo = priv->accountContainer_->info.contactModel->getContact(contactUri);
        auto bestId = std::string(contactInfo.registeredName).empty() ? contactInfo.profileInfo.uri : contactInfo.registeredName;
        if (contactInfo.profileInfo.alias == bestId) {
            gtk_widget_hide(priv->label_cm);
        } else {
            gtk_label_set_text(GTK_LABEL(priv->label_cm), bestId.c_str());
            gtk_widget_show(priv->label_cm);
        }
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
341
    }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
342
343
344
}

static void
345
update_name(ChatView *self)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
346
{
347
    g_return_if_fail(IS_CHAT_VIEW(self));
Stepan Salenikovich's avatar
Stepan Salenikovich committed
348
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
349
    if (!priv->conversation_) return;
350
    auto contactUri = priv->conversation_->participants.front();
351
352
353
354
355
356
357
358
    try {
        auto contactInfo = priv->accountContainer_->info.contactModel->getContact(contactUri);
        auto alias = contactInfo.profileInfo.alias;
        alias.erase(std::remove(alias.begin(), alias.end(), '\r'), alias.end());
        gtk_label_set_text(GTK_LABEL(priv->label_peer), alias.c_str());
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
    }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
359
360
}

aviau's avatar
aviau committed
361
362
363
364
365
366
367
368
369
370
371
372
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)
    );

373
    display_links_toggled(self);
374
375
376
377
    print_text_recording(self);
    load_participants_images(self);

    priv->new_interaction_connection = QObject::connect(
378
    &*priv->accountContainer_->info.conversationModel, &lrc::api::ConversationModel::newInteraction,
379
    [self, priv](const std::string& uid, uint64_t interactionId, lrc::api::interaction::Info interaction) {
380
        if (!priv->conversation_) return;
381
382
383
384
        if(uid == priv->conversation_->uid) {
            print_interaction_to_buffer(self, interactionId, interaction);
        }
    });
385

386
387
388
    priv->update_interaction_connection = QObject::connect(
    &*priv->accountContainer_->info.conversationModel, &lrc::api::ConversationModel::interactionStatusUpdated,
    [self, priv](const std::string& uid, uint64_t msgId, lrc::api::interaction::Info msg) {
389
        if (!priv->conversation_) return;
390
391
392
393
394
        if(uid == priv->conversation_->uid) {
            update_interaction(self, msgId, msg);
        }
    });

395
    if (!priv->conversation_) return;
396
    auto contactUri = priv->conversation_->participants.front();
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
    try {
        auto contactInfo = priv->accountContainer_->info.contactModel->getContact(contactUri);
        priv->isTemporary_ = contactInfo.profileInfo.type == lrc::api::profile::Type::TEMPORARY
            || contactInfo.profileInfo.type == lrc::api::profile::Type::PENDING;
        webkit_chat_container_set_temporary(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container), priv->isTemporary_);
        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());
        webkit_chat_container_set_invitation(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                                             (contactInfo.profileInfo.type == lrc::api::profile::Type::PENDING),
                                             bestName);
        webkit_chat_disable_send_interaction(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                                             (contactInfo.profileInfo.type == lrc::api::profile::Type::SIP)
                                             && priv->conversation_->callId.empty());
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
    }
aviau's avatar
aviau committed
417
418
419
420
421
422
423
424
425
426
}

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);

427
    update_name(self);
428
    update_add_to_conversations(self);
429
    update_contact_methods(self);
aviau's avatar
aviau committed
430

431
    priv->webkit_ready = g_signal_connect_swapped(
aviau's avatar
aviau committed
432
433
434
435
436
437
        priv->webkit_chat_container,
        "ready",
        G_CALLBACK(webkit_chat_container_ready),
        self
    );

438
    priv->webkit_send_text = g_signal_connect(priv->webkit_chat_container,
439
440
        "script-dialog",
        G_CALLBACK(webkit_chat_container_script_dialog),
441
442
        self);

aviau's avatar
aviau committed
443
444
445
    if (webkit_chat_container_is_ready(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container)))
        webkit_chat_container_ready(self);

446
    gtk_widget_set_visible(priv->hbox_chat_info, TRUE);
aviau's avatar
aviau committed
447

448
}
Stepan Salenikovich's avatar
Stepan Salenikovich committed
449
450

GtkWidget *
451
452
453
chat_view_new (WebKitChatContainer* webkit_chat_container,
               AccountContainer* accountContainer,
               lrc::api::conversation::Info* conversation)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
454
455
456
{
    ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));

aviau's avatar
aviau committed
457
458
    ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
    priv->webkit_chat_container = GTK_WIDGET(webkit_chat_container);
459
460
    priv->conversation_ = conversation;
    priv->accountContainer_ = accountContainer;
aviau's avatar
aviau committed
461
462

    build_chat_view(self);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
463
464
465
    return (GtkWidget *)self;
}

466
467
void
chat_view_update_temporary(ChatView* self, bool newValue)
468
{
469
    g_return_if_fail(IS_CHAT_VIEW(self));
470
471
    auto priv = CHAT_VIEW_GET_PRIVATE(self);

472
473
474
475
476
    priv->isTemporary_ = newValue;
    if (!priv->isTemporary_) {
        gtk_widget_hide(priv->button_add_to_conversations);
    }
    webkit_chat_container_set_temporary(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container), priv->isTemporary_);
477
    if (!priv->conversation_) return;
478
    auto contactUri = priv->conversation_->participants.front();
479
480
481
482
483
484
485
486
487
488
489
490
491
    try {
        auto contactInfo = priv->accountContainer_->info.contactModel->getContact(contactUri);
        auto bestName = contactInfo.profileInfo.alias;
        if (bestName.empty())
            bestName = contactInfo.registeredName;
        if (bestName.empty())
            bestName = contactInfo.profileInfo.uri;
        webkit_chat_container_set_invitation(WEBKIT_CHAT_CONTAINER(priv->webkit_chat_container),
                                             newValue,
                                             bestName);
    } catch (const std::out_of_range&) {
        // ContactModel::getContact() exception
    }
492
493
}

494
495
bool
chat_view_get_temporary(ChatView *self)
496
{
497
    g_return_val_if_fail(IS_CHAT_VIEW(self), false);
498
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
499
    return priv->isTemporary_;
500
501
}

502
503
lrc::api::conversation::Info
chat_view_get_conversation(ChatView *self)
504
{
505
    g_return_val_if_fail(IS_CHAT_VIEW(self), lrc::api::conversation::Info());
506
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
507
    return *priv->conversation_;
508
}
509
510
511
512
513
514
515

void
chat_view_set_header_visible(ChatView *self, gboolean visible)
{
    auto priv = CHAT_VIEW_GET_PRIVATE(self);
    gtk_widget_set_visible(priv->hbox_chat_info, visible);
}