webkitchatcontainer.cpp 32.4 KB
Newer Older
aviau's avatar
aviau committed
1
/*
2
 *  Copyright (C) 2016-2021 Savoir-faire Linux Inc.
aviau's avatar
aviau committed
3
 *  Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
4
 *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
5
 *  Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
aviau's avatar
aviau committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 *
 *  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 "webkitchatcontainer.h"

24 25
#include "utils/drawing.h"

aviau's avatar
aviau committed
26 27 28 29
// GTK+ related
#include <webkit2/webkit2.h>

// Qt
30
#include <QtCore/QJsonArray>
aviau's avatar
aviau committed
31 32 33 34 35
#include <QtCore/QJsonValue>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>

// LRC
36
#include <api/conversationmodel.h>
37
#include <api/account.h>
38
#include <api/chatview.h>
aviau's avatar
aviau committed
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

struct _WebKitChatContainer
{
    GtkBox parent;
};

struct _WebKitChatContainerClass
{
    GtkBoxClass parent_class;
};

typedef struct _WebKitChatContainerPrivate WebKitChatContainerPrivate;

struct _WebKitChatContainerPrivate
{
    GtkWidget* webview_chat;
55
    GtkWidget* box_webview_chat;
aviau's avatar
aviau committed
56 57

    bool       chatview_debug;
58
    gchar*     data_received;
aviau's avatar
aviau committed
59 60 61 62 63 64 65 66 67 68 69 70 71

    /* Array of javascript libraries to load. Used during initialization */
    GList*     js_libs_to_load;
    gboolean   js_libs_loaded;
};

G_DEFINE_TYPE_WITH_PRIVATE(WebKitChatContainer, webkit_chat_container, GTK_TYPE_BOX);

#define WEBKIT_CHAT_CONTAINER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), WEBKIT_CHAT_CONTAINER_TYPE, WebKitChatContainerPrivate))

/* signals */
enum {
    READY,
72
    SCRIPT_DIALOG,
73
    DATA_DROPPED,
aviau's avatar
aviau committed
74 75 76 77 78
    LAST_SIGNAL
};

static guint webkit_chat_container_signals[LAST_SIGNAL] = { 0 };

79 80 81
/* functions */
static gboolean webview_crashed(WebKitChatContainer *self);

aviau's avatar
aviau committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
static void
webkit_chat_container_dispose(GObject *object)
{
    G_OBJECT_CLASS(webkit_chat_container_parent_class)->dispose(object);
}

static void
webkit_chat_container_init(WebKitChatContainer *view)
{
    gtk_widget_init_template(GTK_WIDGET(view));
}

static void
webkit_chat_container_class_init(WebKitChatContainerClass *klass)
{
    G_OBJECT_CLASS(klass)->dispose = webkit_chat_container_dispose;

    gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
100
                                                "/net/jami/JamiGnome/webkitchatcontainer.ui");
aviau's avatar
aviau committed
101

102
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), WebKitChatContainer, box_webview_chat);
aviau's avatar
aviau committed
103 104 105 106 107 108 109 110 111 112

    /* add signals */
    webkit_chat_container_signals[READY] = g_signal_new("ready",
        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);
113

114
    webkit_chat_container_signals[SCRIPT_DIALOG] = g_signal_new("script-dialog",
115 116 117 118 119 120 121
        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);
122 123 124 125 126 127 128 129 130

    webkit_chat_container_signals[DATA_DROPPED] = g_signal_new("data-dropped",
        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);
aviau's avatar
aviau committed
131 132 133
}

static gboolean
134
webview_chat_context_menu(G_GNUC_UNUSED WebKitChatContainer *self,
135
                          WebKitContextMenu   *menu,
136 137 138
                          G_GNUC_UNUSED GdkEvent            *event,
                          G_GNUC_UNUSED WebKitHitTestResult *hit_test_result,
                          G_GNUC_UNUSED gpointer             user_data)
aviau's avatar
aviau committed
139
{
140 141 142 143 144 145 146 147 148 149 150 151 152 153
    GList *items, *nextList;
    for (items = webkit_context_menu_get_items(menu) ; items ; items = nextList) {
        WebKitContextMenuAction action;
        nextList = items->next;
        auto item = (WebKitContextMenuItem*)items->data;
        action = webkit_context_menu_item_get_stock_action(item);

        if (action == WEBKIT_CONTEXT_MENU_ACTION_RELOAD ||
        action == WEBKIT_CONTEXT_MENU_ACTION_GO_FORWARD ||
        action == WEBKIT_CONTEXT_MENU_ACTION_GO_BACK ||
        action == WEBKIT_CONTEXT_MENU_ACTION_STOP) {
            webkit_context_menu_remove(menu, item);
        }
    }
154

Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
155 156
    // FALSE = custom menu, TRUE would mean no menu
    return FALSE;
aviau's avatar
aviau committed
157 158
}

159
static void
160
webkit_chat_container_execute_js(WebKitChatContainer *view, const gchar* function_call)
161 162 163 164 165 166 167 168 169 170 171
{
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
    webkit_web_view_run_javascript(
        WEBKIT_WEB_VIEW(priv->webview_chat),
        function_call,
        NULL,
        NULL,
        NULL
    );
}

172
QJsonObject
173
build_interaction_json(lrc::api::ConversationModel& conversation_model,
Amin Bandali's avatar
Amin Bandali committed
174 175
                       const QString& convId,
                       const QString& msgId,
176
                       const lrc::api::interaction::Info& interaction)
aviau's avatar
aviau committed
177
{
178
    auto sender = interaction.authorUri;
179
    if (sender == "") {
180
        sender = conversation_model.owner.profileInfo.uri;
181
    }
182
    auto timestamp = QString::number(interaction.timestamp);
183
    QString direction = lrc::api::interaction::isOutgoing(interaction) ? "out" : "in";
184 185

    QJsonObject interaction_object = QJsonObject();
186
    interaction_object.insert("text", QJsonValue(interaction.body));
Amin Bandali's avatar
Amin Bandali committed
187
    interaction_object.insert("id", QJsonValue(msgId));
188
    interaction_object.insert("sender", QJsonValue(sender));
Sébastien Blin's avatar
Sébastien Blin committed
189
    interaction_object.insert("duration", QJsonValue(static_cast<int>(interaction.duration)));
190 191 192
    interaction_object.insert("sender_contact_method", QJsonValue(sender));
    interaction_object.insert("timestamp", QJsonValue(timestamp));
    interaction_object.insert("direction", QJsonValue(direction));
193

194
    switch (interaction.type)
195
    {
196 197 198 199 200 201 202 203 204
    case lrc::api::interaction::Type::TEXT:
        interaction_object.insert("type", QJsonValue("text"));
        break;
    case lrc::api::interaction::Type::CALL:
        interaction_object.insert("type", QJsonValue("call"));
        break;
    case lrc::api::interaction::Type::CONTACT:
        interaction_object.insert("type", QJsonValue("contact"));
        break;
205
    case lrc::api::interaction::Type::DATA_TRANSFER: {
206 207
        interaction_object.insert("type", QJsonValue("data_transfer"));
        lrc::api::datatransfer::Info info = {};
Amin Bandali's avatar
Amin Bandali committed
208
        conversation_model.getTransferInfo(convId, msgId, info);
209 210 211 212
        if (info.status != lrc::api::datatransfer::Status::INVALID) {
            interaction_object.insert("totalSize", QJsonValue(qint64(info.totalSize)));
            interaction_object.insert("progress", QJsonValue(qint64(info.progress)));
        }
213
        interaction_object.insert("displayName", QJsonValue(interaction.commit["displayName"]));
214 215
        break;
    }
216 217 218 219
    case lrc::api::interaction::Type::INVALID:
    default:
        interaction_object.insert("type", QJsonValue(""));
        break;
220
    }
221

222 223 224 225
    if (interaction.isRead) {
        interaction_object.insert("delivery_status", QJsonValue("read"));
    }

226
    switch (interaction.status)
227
    {
228
    case lrc::api::interaction::Status::SUCCESS:
229 230
        interaction_object.insert("delivery_status", QJsonValue("sent"));
        break;
231
    case lrc::api::interaction::Status::FAILURE:
232
    case lrc::api::interaction::Status::TRANSFER_ERROR:
233 234
        interaction_object.insert("delivery_status", QJsonValue("failure"));
        break;
235 236 237
    case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
        interaction_object.insert("delivery_status", QJsonValue("unjoinable peer"));
        break;
238 239 240
    case lrc::api::interaction::Status::SENDING:
        interaction_object.insert("delivery_status", QJsonValue("sending"));
        break;
241 242 243 244 245 246 247 248 249 250 251 252
    case lrc::api::interaction::Status::TRANSFER_CREATED:
        interaction_object.insert("delivery_status", QJsonValue("connecting"));
        break;
    case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
        interaction_object.insert("delivery_status", QJsonValue("accepted"));
        break;
    case lrc::api::interaction::Status::TRANSFER_CANCELED:
        interaction_object.insert("delivery_status", QJsonValue("canceled"));
        break;
    case lrc::api::interaction::Status::TRANSFER_ONGOING:
        interaction_object.insert("delivery_status", QJsonValue("ongoing"));
        break;
253 254 255 256 257
    case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
        interaction_object.insert("delivery_status", QJsonValue("awaiting peer"));
        break;
    case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST:
        interaction_object.insert("delivery_status", QJsonValue("awaiting host"));
258
        break;
259 260 261
    case lrc::api::interaction::Status::TRANSFER_TIMEOUT_EXPIRED:
        interaction_object.insert("delivery_status", QJsonValue("awaiting peer timeout"));
        break;
262 263 264
    case lrc::api::interaction::Status::TRANSFER_FINISHED:
        interaction_object.insert("delivery_status", QJsonValue("finished"));
        break;
265 266 267 268 269
    case lrc::api::interaction::Status::INVALID:
    case lrc::api::interaction::Status::UNKNOWN:
    default:
        interaction_object.insert("delivery_status", QJsonValue("unknown"));
        break;
270
    }
271 272
    return interaction_object;
}
273

274
QString
275
interaction_to_json_interaction_object(lrc::api::ConversationModel& conversation_model,
Amin Bandali's avatar
Amin Bandali committed
276 277
                                       const QString& convId,
                                       const QString& msgId,
278
                                       const lrc::api::interaction::Info& interaction)
279
{
Amin Bandali's avatar
Amin Bandali committed
280
    auto interaction_object = build_interaction_json(conversation_model, convId, msgId, interaction);
281
    return QString(QJsonDocument(interaction_object).toJson(QJsonDocument::Compact));
aviau's avatar
aviau committed
282 283
}

284
QString
285
interactions_to_json_array_object(lrc::api::ConversationModel& conversation_model,
286
                                  const QString& convId,
287
                                  std::unique_ptr<lrc::api::MessageListModel>& interactions) {
288
    QJsonArray array;
289
    for (const auto& interaction: *interactions.get())
290
        array.append(build_interaction_json(conversation_model, convId, interaction.first, interaction.second));
291 292 293
    return QString(QJsonDocument(array).toJson(QJsonDocument::Compact));
}

294
#if WEBKIT_CHECK_VERSION(2, 6, 0)
aviau's avatar
aviau committed
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
static gboolean
webview_chat_decide_policy (G_GNUC_UNUSED WebKitWebView *web_view,
                            WebKitPolicyDecision *decision,
                            WebKitPolicyDecisionType type)
{
    switch (type)
    {
        case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
        case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
        {
            WebKitNavigationPolicyDecision* navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
            WebKitNavigationAction* navigation_action = webkit_navigation_policy_decision_get_navigation_action(navigation_decision);
            WebKitNavigationType navigation_type = webkit_navigation_action_get_navigation_type(navigation_action);

            switch (navigation_type)
            {
                case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
                case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
                case WEBKIT_NAVIGATION_TYPE_RELOAD:
                case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
                case WEBKIT_NAVIGATION_TYPE_OTHER:
                {
                    /* make no decision */
                    return FALSE;

                }
                case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
                {
                    webkit_policy_decision_ignore(decision);

                    WebKitURIRequest* uri_request = webkit_navigation_action_get_request(navigation_action);
                    const gchar* uri = webkit_uri_request_get_uri(uri_request);

                    gtk_show_uri(NULL, uri, GDK_CURRENT_TIME, NULL);
                }
            }

            webkit_policy_decision_ignore(decision);
            break;
        }
        case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
        {
            return FALSE;
        }
        default:
        {
            /* Making no decision results in webkit_policy_decision_use(). */
            return FALSE;
        }
    }
    return TRUE;
}
#endif

349
static gboolean
350 351 352
webview_script_dialog(WebKitWebView      *self,
                      WebKitScriptDialog *dialog,
                      G_GNUC_UNUSED gpointer user_data)
353
{
354 355
    auto interaction = webkit_script_dialog_get_message(dialog);
    g_signal_emit(G_OBJECT(self), webkit_chat_container_signals[SCRIPT_DIALOG], 0, interaction);
356 357 358
    return true;
}

359 360 361 362 363
static void
init_js_i18n(WebKitChatContainer *view)
{
    gchar *function_call;

Sébastien Blin's avatar
Sébastien Blin committed
364
    auto translated = lrc::api::chatview::getTranslatedStrings(false);
365 366 367 368 369 370
    QJsonObject trjson;
    for (auto i = translated.begin(); i != translated.end(); ++i) {
        QString value = i.value().toString();
        if (not value.isEmpty()) {
            trjson[i.key()] = value;
        }
371 372
    }

373 374 375 376
    QJsonDocument doc(trjson);
    QString strJson(doc.toJson(QJsonDocument::Compact));

    if (strJson.isEmpty()) {
377 378 379
        /* no translation available for current locale, use default */
        function_call = g_strdup("init_i18n()");
    } else {
380
        function_call = g_strdup_printf("init_i18n(%s)", qUtf8Printable(strJson));
381 382 383 384 385 386 387
    }

    webkit_chat_container_execute_js(view, function_call);

    g_free(function_call);
}

aviau's avatar
aviau committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
static void
javascript_library_loaded(WebKitWebView *webview_chat,
                          GAsyncResult *result,
                          WebKitChatContainer* self)
{
    g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(self));
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);

    auto loaded_library = g_list_first(priv->js_libs_to_load);

    GError *error = NULL;
    WebKitJavascriptResult* js_result = webkit_web_view_run_javascript_from_gresource_finish(webview_chat, result, &error);
    if (!js_result) {
        g_warning("Error loading %s: %s", (const gchar*) loaded_library->data, error->message);
        g_error_free(error);
        g_object_unref(self);
        /* Stop loading view, most likely resulting in a blank page */
        return;
    }
    webkit_javascript_result_unref(js_result);

    priv->js_libs_to_load = g_list_remove(priv->js_libs_to_load, loaded_library->data);

    if(g_list_length(priv->js_libs_to_load) > 0)
    {
        /* keep loading... */
        webkit_web_view_run_javascript_from_gresource(
            webview_chat,
            (const gchar*) g_list_first(priv->js_libs_to_load)->data,
            NULL,
            (GAsyncReadyCallback) javascript_library_loaded,
            self
        );
    }
    else
    {
424 425 426
         /* load translations before anything else */
         init_js_i18n(self);

aviau's avatar
aviau committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
         priv->js_libs_loaded = TRUE;
         g_signal_emit(G_OBJECT(self), webkit_chat_container_signals[READY], 0);

         /* The view could now be deleted without causing a crash */
         g_object_unref(self);
    }
}

static void
load_javascript_libs(WebKitWebView *webview_chat,
                     WebKitChatContainer* self)
{
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);

    /* Create the list of libraries to load */
442
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/jed.js");
443
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/linkify.js");
444
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/emoji.js");
Sébastien Blin's avatar
Sébastien Blin committed
445
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/chatview.js");
446 447
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/linkify-string.js");
    priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/net/jami/JamiGnome/linkify-html.js");
aviau's avatar
aviau committed
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

    /* ref the chat view so that its not destroyed while we load
     * we will unref in javascript_library_loaded
     */
    g_object_ref(self);

   /* start loading */
    webkit_web_view_run_javascript_from_gresource(
        WEBKIT_WEB_VIEW(webview_chat),
        (const gchar*) g_list_first(priv->js_libs_to_load)->data,
        NULL,
        (GAsyncReadyCallback) javascript_library_loaded,
        self
    );
}

static void
webview_chat_load_changed(WebKitWebView  *webview_chat,
                          WebKitLoadEvent load_event,
                          WebKitChatContainer* self)
{
    switch (load_event) {
        case WEBKIT_LOAD_REDIRECTED:
        {
            g_warning("webview_chat load is being redirected, this should not happen");
        }
        case WEBKIT_LOAD_STARTED:
        case WEBKIT_LOAD_COMMITTED:
        {
            break;
        }
        case WEBKIT_LOAD_FINISHED:
        {
            load_javascript_libs(webview_chat, self);
            //TODO: disconnect? It shouldn't happen more than once
            break;
        }
    }
}

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
static void
webview_chat_on_drag_data_received(GtkWidget*,
                                   GdkDragContext*,
                                   gint,
                                   gint,
                                   GtkSelectionData *data,
                                   guint,
                                   guint32,
                                   WebKitChatContainer* self)
{
    g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(self));
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
    auto* filename = (gchar*)(gtk_selection_data_get_data(data));
    if (filename) {
        priv->data_received = g_strdup_printf("%s", filename);
    }
}


static gboolean
webview_chat_on_drag_data(GtkWidget*,
                          GdkDragContext*,
                          gint,
                          gint,
                          guint,
                          WebKitChatContainer* self)
{
    g_return_val_if_fail(IS_WEBKIT_CHAT_CONTAINER(self), true);
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
    g_signal_emit(G_OBJECT(self), webkit_chat_container_signals[DATA_DROPPED], 0, priv->data_received);
    return true;
}

aviau's avatar
aviau committed
521 522 523 524 525 526 527
static void
build_view(WebKitChatContainer *view)
{
    g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(view));
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);

    priv->chatview_debug = FALSE;
Sébastien Blin's avatar
Sébastien Blin committed
528 529
    auto chatview_debug = g_getenv("CHATVIEW_DEBUG");
    if (chatview_debug || g_strcmp0(chatview_debug, "true") == 0)
aviau's avatar
aviau committed
530 531 532 533
    {
        priv->chatview_debug = TRUE;
    }

534 535 536 537 538 539
    /* Prepare WebKitUserContentManager */
    WebKitUserContentManager* webkit_content_manager = webkit_user_content_manager_new();

    WebKitUserStyleSheet* chatview_style_sheet = webkit_user_style_sheet_new(
        (gchar*) g_bytes_get_data(
            g_resources_lookup_data(
540
                "/net/jami/JamiGnome/chatview.css",
541
                G_RESOURCE_LOOKUP_FLAGS_NONE,
Sébastien Blin's avatar
Sébastien Blin committed
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
                NULL
            ),
            NULL
        ),
        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
        WEBKIT_USER_STYLE_LEVEL_USER,
        NULL,
        NULL
    );
    webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);

    chatview_style_sheet = webkit_user_style_sheet_new(
        (gchar*) g_bytes_get_data(
            g_resources_lookup_data(
                "/net/jami/JamiGnome/chatview-gnome.css",
                G_RESOURCE_LOOKUP_FLAGS_NONE,
558 559 560 561 562 563 564 565 566 567 568
                NULL
            ),
            NULL
        ),
        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
        WEBKIT_USER_STYLE_LEVEL_USER,
        NULL,
        NULL
    );
    webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);

569 570 571 572 573 574 575 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
    chatview_style_sheet = webkit_user_style_sheet_new(
        (gchar*) g_bytes_get_data(
            g_resources_lookup_data(
                "/net/jami/JamiGnome/emoji.css",
                G_RESOURCE_LOOKUP_FLAGS_NONE,
                NULL
            ),
            NULL
        ),
        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
        WEBKIT_USER_STYLE_LEVEL_USER,
        NULL,
        NULL
    );
    webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);

    chatview_style_sheet = webkit_user_style_sheet_new(
        (gchar*) g_bytes_get_data(
            g_resources_lookup_data(
                "/net/jami/JamiGnome/fa.css",
                G_RESOURCE_LOOKUP_FLAGS_NONE,
                NULL
            ),
            NULL
        ),
        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
        WEBKIT_USER_STYLE_LEVEL_USER,
        NULL,
        NULL
    );
    webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);

601
    /* Prepare WebKitSettings */
aviau's avatar
aviau committed
602 603 604 605 606 607 608
    WebKitSettings* webkit_settings = webkit_settings_new_with_settings(
        "enable-javascript", TRUE,
        "enable-developer-extras", priv->chatview_debug,
        "enable-java", FALSE,
        "enable-plugins", FALSE,
        "enable-site-specific-quirks", FALSE,
        "enable-smooth-scrolling", TRUE,
609
        "enable-html5-local-storage", TRUE,
aviau's avatar
aviau committed
610 611
        NULL
    );
612 613 614 615 616 617 618 619 620 621 622 623 624 625

    /* Create the WebKitWebView */
    priv->webview_chat = GTK_WIDGET(
        webkit_web_view_new_with_user_content_manager(
            webkit_content_manager
        )
    );

    gtk_container_add(GTK_CONTAINER(priv->box_webview_chat), priv->webview_chat);
    gtk_widget_show(priv->webview_chat);
    gtk_widget_set_vexpand(GTK_WIDGET(priv->webview_chat), TRUE);
    gtk_widget_set_hexpand(GTK_WIDGET(priv->webview_chat), TRUE);

    /* Set the WebKitSettings */
aviau's avatar
aviau committed
626 627
    webkit_web_view_set_settings(WEBKIT_WEB_VIEW(priv->webview_chat), webkit_settings);

628 629
    g_signal_connect(priv->webview_chat, "drag-data-received", G_CALLBACK(webview_chat_on_drag_data_received), view);
    g_signal_connect(priv->webview_chat, "drag-drop", G_CALLBACK(webview_chat_on_drag_data), view);
aviau's avatar
aviau committed
630 631
    g_signal_connect(priv->webview_chat, "load-changed", G_CALLBACK(webview_chat_load_changed), view);
    g_signal_connect_swapped(priv->webview_chat, "context-menu", G_CALLBACK(webview_chat_context_menu), view);
632
    g_signal_connect_swapped(priv->webview_chat, "script-dialog", G_CALLBACK(webview_script_dialog), view);
633
#if WEBKIT_CHECK_VERSION(2, 6, 0)
aviau's avatar
aviau committed
634 635 636 637
    g_signal_connect(priv->webview_chat, "decide-policy", G_CALLBACK(webview_chat_decide_policy), view);
#endif

    GBytes* chatview_bytes = g_resources_lookup_data(
638
        "/net/jami/JamiGnome/chatview.html",
aviau's avatar
aviau committed
639 640 641 642
        G_RESOURCE_LOOKUP_FLAGS_NONE,
        NULL
    );

643
    // file:// allow the webview to load local files
aviau's avatar
aviau committed
644 645 646
    webkit_web_view_load_html(
        WEBKIT_WEB_VIEW(priv->webview_chat),
        (gchar*) g_bytes_get_data(chatview_bytes, NULL),
647
        "file://"
aviau's avatar
aviau committed
648 649 650 651
    );

    /* Now we wait for the load-changed event, before we
     * start loading javascript libraries */
652 653 654 655 656 657 658 659

    /* handle web view crash */
    g_signal_connect_swapped(priv->webview_chat, "web-process-crashed", G_CALLBACK(webview_crashed), view);
}

static gboolean
webview_crashed(WebKitChatContainer *self)
{
Philippe Gorley's avatar
Philippe Gorley committed
660
    g_warning("Gtk Web Process crashed! Recreating web view");
661 662 663 664 665 666 667 668 669 670 671 672

    auto priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);

    /* make sure we destroy previous WebView */
    if (priv->webview_chat) {
        gtk_widget_destroy(priv->webview_chat);
        priv->webview_chat = nullptr;
    }

    build_view(self);

    return G_SOURCE_CONTINUE;
aviau's avatar
aviau committed
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
}

GtkWidget *
webkit_chat_container_new()
{
    gpointer view = g_object_new(WEBKIT_CHAT_CONTAINER_TYPE, NULL);

    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
    priv->js_libs_loaded = FALSE;

    build_view(WEBKIT_CHAT_CONTAINER(view));

    return (GtkWidget *)view;
}

688 689 690
void
webkit_chat_container_set_display_links(WebKitChatContainer *view, bool display)
{
691
    gchar* function_call = g_strdup_printf("setDisplayLinks(%s);",
692
      display ? "true" : "false");
693 694
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
695 696
}

aviau's avatar
aviau committed
697
void
698
webkit_chat_container_clear_sender_images(WebKitChatContainer *view)
aviau's avatar
aviau committed
699
{
700
    webkit_chat_container_execute_js(view, "clearSenderImages();");
701 702 703 704 705
}

void
webkit_chat_container_clear(WebKitChatContainer *view)
{
706
    webkit_chat_container_execute_js(view, "clearMessages();");
707
    webkit_chat_container_clear_sender_images(view);
aviau's avatar
aviau committed
708 709 710
}

void
711
webkit_chat_container_update_interaction(WebKitChatContainer *view,
712
                                         lrc::api::ConversationModel& conversation_model,
713
                                         const QString& convId,
Amin Bandali's avatar
Amin Bandali committed
714
                                         const QString& msgId,
715
                                         const lrc::api::interaction::Info& interaction)
aviau's avatar
aviau committed
716
{
717
    auto interaction_object = interaction_to_json_interaction_object(conversation_model, convId, msgId, interaction).toUtf8();
718
    gchar* function_call = g_strdup_printf("updateMessage(%s);", interaction_object.constData());
719
    webkit_chat_container_execute_js(view, function_call);
aviau's avatar
aviau committed
720 721 722
    g_free(function_call);
}

723
void
Amin Bandali's avatar
Amin Bandali committed
724
webkit_chat_container_remove_interaction(WebKitChatContainer *view, const QString& interactionId)
725
{
726
    gchar* function_call = g_strdup_printf("removeInteraction(%lu);", interactionId);
727
    webkit_chat_container_execute_js(view, function_call);
728 729 730 731
    g_free(function_call);
}


aviau's avatar
aviau committed
732
void
733
webkit_chat_container_print_new_interaction(WebKitChatContainer *view,
734
                                            lrc::api::ConversationModel& conversation_model,
735
                                            const QString& convId,
Amin Bandali's avatar
Amin Bandali committed
736
                                            const QString& msgId,
737
                                            const lrc::api::interaction::Info& interaction)
aviau's avatar
aviau committed
738
{
739
    auto interaction_object = interaction_to_json_interaction_object(conversation_model, convId, msgId, interaction).toUtf8();
740
    gchar* function_call = g_strdup_printf("addMessage(%s);", interaction_object.constData());
741
    webkit_chat_container_execute_js(view, function_call);
aviau's avatar
aviau committed
742 743 744
    g_free(function_call);
}

745
void
746 747
webkit_chat_container_print_history(WebKitChatContainer *view,
                                    lrc::api::ConversationModel& conversation_model,
748
                                    const QString& convId,
749
                                    std::unique_ptr<lrc::api::MessageListModel>& interactions)
750
{
751
    auto interactions_str = interactions_to_json_array_object(conversation_model, convId, interactions).toUtf8();
752
    gchar* function_call = g_strdup_printf("printHistory(%s)", interactions_str.constData());
753
    webkit_chat_container_execute_js(view, function_call);
754 755 756
    g_free(function_call);
}

757 758 759
void
webkit_chat_container_update_history(WebKitChatContainer *view,
                                     lrc::api::ConversationModel& conversation_model,
760
                                     const QString& convId,
761
                                     std::unique_ptr<lrc::api::MessageListModel>& interactions,
762 763
                                     bool all_loaded)
{
764
    auto interactions_str = interactions_to_json_array_object(conversation_model, convId, interactions).toUtf8();
765 766 767 768 769 770 771
    gchar* function_call = g_strdup_printf("updateHistory(%s, %s)",
                                           interactions_str.constData(),
                                           all_loaded ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

772
void
773 774 775 776
webkit_chat_container_set_invitation(WebKitChatContainer *view,
                                     bool show,
                                     const std::string& bestName,
                                     const std::string& bestId)
777
{
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
778
    // TODO better escape names
779 780 781 782
    gchar* function_call = g_strdup_printf(
        show ? "showInvitation(\"%s\", \"%s\")" : "showInvitation()",
        bestName.c_str(),
        bestId.c_str());
783
    webkit_chat_container_execute_js(view, function_call);
784 785 786 787 788 789 790
    g_free(function_call);
}

void
webkit_chat_container_set_sender_image(WebKitChatContainer *view, const std::string& sender, const std::string& senderImage)
{
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
aviau's avatar
aviau committed
791 792

    QJsonObject set_sender_image_object = QJsonObject();
793 794
    set_sender_image_object.insert("sender_contact_method", QJsonValue(QString(sender.c_str())));
    set_sender_image_object.insert("sender_image", QJsonValue(QString(senderImage.c_str())));
aviau's avatar
aviau committed
795

796
    auto set_sender_image_object_string = QString(QJsonDocument(set_sender_image_object).toJson(QJsonDocument::Compact));
aviau's avatar
aviau committed
797

798
    gchar* function_call = g_strdup_printf("setSenderImage(%s);", set_sender_image_object_string.toUtf8().constData());
aviau's avatar
aviau committed
799 800 801 802 803 804 805 806 807 808
    webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(priv->webview_chat), function_call, NULL, NULL, NULL);
    g_free(function_call);
}

gboolean
webkit_chat_container_is_ready(WebKitChatContainer *view)
{
    WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
    return priv->js_libs_loaded;
}
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
809 810 811 812 813

void
webkit_chat_set_header_visible(WebKitChatContainer *view, bool isVisible)
{
    gchar* function_call = g_strdup_printf("displayNavbar(%s)", isVisible ? "true" : "false");
814
    webkit_chat_container_execute_js(view, function_call);
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
815 816 817
    g_free(function_call);
}

818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
void
webkit_chat_hide_controls(WebKitChatContainer *view, bool hide)
{
    gchar* function_call = g_strdup_printf("hideControls(%s)", hide ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

void
webkit_chat_hide_message_bar(WebKitChatContainer *view, bool hide)
{
    gchar* function_call = g_strdup_printf("hideMessageBar(%s)", hide ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

Sébastien Blin's avatar
Sébastien Blin committed
834 835 836 837 838 839 840 841
void
webkit_chat_set_record_visible(WebKitChatContainer *view, bool isVisible)
{
    gchar* function_call = g_strdup_printf("displayRecordControls(%s)", isVisible ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

842 843 844 845 846 847 848 849
void
webkit_chat_set_plugin_visible(WebKitChatContainer *view, bool isVisible)
{
    gchar* function_call = g_strdup_printf("displayPluginControl(%s)", isVisible ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

850 851 852 853 854 855
void
webkit_chat_set_dark_mode(WebKitChatContainer *view, bool darkMode, const std::string& background)
{
    std::string theme = "";
    if (darkMode) {
        theme = "\
856
            --svg-invert-percentage: 1;\
857 858 859 860 861 862 863 864 865 866
            --jami-light-blue: #003b4e;\
            --jami-dark-blue: #28b1ed;\
            --text-color: white;\
            --timestamp-color: #bbb;\
            --message-out-bg: #28b1ed;\
            --message-out-txt: white;\
            --message-in-bg: #616161;\
            --message-in-txt: white;\
            --file-in-timestamp-color: #999;\
            --file-out-timestamp-color: #eee;\
867
            --bg-invitation-rect: #222;\
868 869 870 871 872 873 874
            --bg-color: " + background + ";\
            --non-action-icon-color: white;\
            --placeholder-text-color: #2b2b2b;\
            --invite-hover-color: black;\
            --hairline-color: #262626;\
        ";
    }
875 876
    gchar* function_call = g_strdup_printf("setTheme(\"%s\"); init_picker(%s)",
                                           theme.c_str(), darkMode ? "true" : "false");
877 878 879 880
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

881 882 883 884 885 886 887 888
void
webkit_chat_set_is_swarm(WebKitChatContainer *view, bool isSwarm)
{
    gchar* function_call = g_strdup_printf("set_is_swarm(%s)", isSwarm ? "true" : "false");
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

889 890 891
void
webkit_chat_set_is_composing(WebKitChatContainer *view, const std::string& contactUri, bool isComposing)
{
892
    gchar* function_call = g_strdup_printf("showTypingIndicator(\"%s\", %s)", contactUri.c_str(), isComposing ? "1" : "0");
893 894 895 896
    webkit_chat_container_execute_js(view, function_call);
    g_free(function_call);
}

Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
897
void
898 899 900 901 902 903 904 905 906 907 908 909
webkit_chat_update_chatview_frame(WebKitChatContainer *view,
                                  bool accountEnabled, bool isBanned,
                                  bool isInvited,
                                  const gchar* bestName,
                                  const gchar* bestId)
{
    gchar* function_call = g_strdup_printf(
        "update_chatview_frame(%s, %s, %s, \"%s\", \"%s\")",
        accountEnabled ? "true" : "false",
        isBanned ? "true" : "false",
        isInvited ? "true" : "false",
        bestName, bestId);
910
    webkit_chat_container_execute_js(view, function_call);
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
911 912
    g_free(function_call);
}