blob: fcb8f2408e3bc72bfbb7b39a551cbe2e56244af1 [file] [log] [blame]
aviau039001d2016-09-29 16:39:05 -04001/*
2 * Copyright (C) 2016 Savoir-faire Linux Inc.
3 * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "webkitchatcontainer.h"
21
22// GTK+ related
23#include <gtk/gtk.h>
24#include <glib/gi18n.h>
25#include <webkit2/webkit2.h>
26
27// Qt
28#include <QtCore/QJsonValue>
29#include <QtCore/QJsonObject>
30#include <QtCore/QJsonDocument>
31
32// LRC
33#include <media/textrecording.h>
34#include <globalinstances.h>
aviaufc213552016-11-01 12:39:39 -040035#include <contactmethod.h>
aviau039001d2016-09-29 16:39:05 -040036
37// Ring Client
38#include "native/pixbufmanipulator.h"
39#include "config.h"
40
41struct _WebKitChatContainer
42{
43 GtkBox parent;
44};
45
46struct _WebKitChatContainerClass
47{
48 GtkBoxClass parent_class;
49};
50
51typedef struct _WebKitChatContainerPrivate WebKitChatContainerPrivate;
52
53struct _WebKitChatContainerPrivate
54{
55 GtkWidget* webview_chat;
aviau32b8dd62016-11-02 00:36:57 -040056 GtkWidget* box_webview_chat;
aviau039001d2016-09-29 16:39:05 -040057
58 bool chatview_debug;
59
60 /* Array of javascript libraries to load. Used during initialization */
61 GList* js_libs_to_load;
62 gboolean js_libs_loaded;
63};
64
65G_DEFINE_TYPE_WITH_PRIVATE(WebKitChatContainer, webkit_chat_container, GTK_TYPE_BOX);
66
67#define WEBKIT_CHAT_CONTAINER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), WEBKIT_CHAT_CONTAINER_TYPE, WebKitChatContainerPrivate))
68
69/* signals */
70enum {
71 READY,
72 LAST_SIGNAL
73};
74
75static guint webkit_chat_container_signals[LAST_SIGNAL] = { 0 };
76
77static void
78webkit_chat_container_dispose(GObject *object)
79{
80 G_OBJECT_CLASS(webkit_chat_container_parent_class)->dispose(object);
81}
82
83static void
84webkit_chat_container_init(WebKitChatContainer *view)
85{
86 gtk_widget_init_template(GTK_WIDGET(view));
87}
88
89static void
90webkit_chat_container_class_init(WebKitChatContainerClass *klass)
91{
92 G_OBJECT_CLASS(klass)->dispose = webkit_chat_container_dispose;
93
aviau039001d2016-09-29 16:39:05 -040094 gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
95 "/cx/ring/RingGnome/webkitchatcontainer.ui");
96
aviau32b8dd62016-11-02 00:36:57 -040097 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), WebKitChatContainer, box_webview_chat);
aviau039001d2016-09-29 16:39:05 -040098
99 /* add signals */
100 webkit_chat_container_signals[READY] = g_signal_new("ready",
101 G_TYPE_FROM_CLASS(klass),
102 (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
103 0,
104 nullptr,
105 nullptr,
106 g_cclosure_marshal_VOID__VOID,
107 G_TYPE_NONE, 0);
108}
109
110static gboolean
111webview_chat_context_menu(WebKitChatContainer *self)
112{
113 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
114 return !priv->chatview_debug;
115}
116
117QString
118message_index_to_json_message_object(const QModelIndex &idx)
119{
120 auto message = idx.data().value<QString>();
121 auto sender = idx.data(static_cast<int>(Media::TextRecording::Role::AuthorDisplayname)).value<QString>();
aviaufc213552016-11-01 12:39:39 -0400122 auto sender_contact_method = idx.data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod*>();
aviau039001d2016-09-29 16:39:05 -0400123 auto timestamp = idx.data(static_cast<int>(Media::TextRecording::Role::Timestamp)).value<time_t>();
124 auto direction = idx.data(static_cast<int>(Media::TextRecording::Role::Direction)).value<Media::Media::Direction>();
125 auto message_id = idx.row();
126
aviaufc213552016-11-01 12:39:39 -0400127 QString sender_contact_method_str;
128 if(direction == Media::Media::Direction::IN)
129 {
130 sender_contact_method_str = QString(g_strdup_printf("%p", sender_contact_method));
131 }
132 else
133 {
134 sender_contact_method_str = "self";
135 }
136
aviau039001d2016-09-29 16:39:05 -0400137 QJsonObject message_object = QJsonObject();
138 message_object.insert("text", QJsonValue(message));
139 message_object.insert("id", QJsonValue(QString().setNum(message_id)));
140 message_object.insert("sender", QJsonValue(sender));
aviaufc213552016-11-01 12:39:39 -0400141 message_object.insert("sender_contact_method", QJsonValue(sender_contact_method_str));
aviau039001d2016-09-29 16:39:05 -0400142 message_object.insert("timestamp", QJsonValue((int) timestamp));
143 message_object.insert("direction", QJsonValue((direction == Media::Media::Direction::IN) ? "in" : "out"));
144
145 switch(idx.data(static_cast<int>(Media::TextRecording::Role::DeliveryStatus)).value<Media::TextRecording::Status>())
146 {
147 case Media::TextRecording::Status::FAILURE:
148 {
149 message_object.insert("delivery_status", QJsonValue("failure"));
150 break;
151 }
152 case Media::TextRecording::Status::COUNT__:
153 {
154 message_object.insert("delivery_status", QJsonValue("count__"));
155 break;
156 }
157 case Media::TextRecording::Status::SENDING:
158 {
159 message_object.insert("delivery_status", QJsonValue("sending"));
160 break;
161 }
162 case Media::TextRecording::Status::UNKNOWN:
163 {
164 message_object.insert("delivery_status", QJsonValue("unknown"));
165 break;
166 }
167 case Media::TextRecording::Status::READ:
168 {
169 message_object.insert("delivery_status", QJsonValue("read"));
170 break;
171 }
172 case Media::TextRecording::Status::SENT:
173 {
174 message_object.insert("delivery_status", QJsonValue("sent"));
175 break;
176 }
177 }
178
179 return QString(QJsonDocument(message_object).toJson(QJsonDocument::Compact));
180}
181
182#if HAVE_WEBKIT2GTK4
183static gboolean
184webview_chat_decide_policy (G_GNUC_UNUSED WebKitWebView *web_view,
185 WebKitPolicyDecision *decision,
186 WebKitPolicyDecisionType type)
187{
188 switch (type)
189 {
190 case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
191 case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
192 {
193 WebKitNavigationPolicyDecision* navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
194 WebKitNavigationAction* navigation_action = webkit_navigation_policy_decision_get_navigation_action(navigation_decision);
195 WebKitNavigationType navigation_type = webkit_navigation_action_get_navigation_type(navigation_action);
196
197 switch (navigation_type)
198 {
199 case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
200 case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
201 case WEBKIT_NAVIGATION_TYPE_RELOAD:
202 case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
203 case WEBKIT_NAVIGATION_TYPE_OTHER:
204 {
205 /* make no decision */
206 return FALSE;
207
208 }
209 case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
210 {
211 webkit_policy_decision_ignore(decision);
212
213 WebKitURIRequest* uri_request = webkit_navigation_action_get_request(navigation_action);
214 const gchar* uri = webkit_uri_request_get_uri(uri_request);
215
216 gtk_show_uri(NULL, uri, GDK_CURRENT_TIME, NULL);
217 }
218 }
219
220 webkit_policy_decision_ignore(decision);
221 break;
222 }
223 case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
224 {
225 //WebKitResponsePolicyDecision *response = WEBKIT_RESPONSE_POLICY_DECISION (decision);
226 //break;
227 return FALSE;
228 }
229 default:
230 {
231 /* Making no decision results in webkit_policy_decision_use(). */
232 return FALSE;
233 }
234 }
235 return TRUE;
236}
237#endif
238
239static void
240javascript_library_loaded(WebKitWebView *webview_chat,
241 GAsyncResult *result,
242 WebKitChatContainer* self)
243{
244 g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(self));
245 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
246
247 auto loaded_library = g_list_first(priv->js_libs_to_load);
248
249 GError *error = NULL;
250 WebKitJavascriptResult* js_result = webkit_web_view_run_javascript_from_gresource_finish(webview_chat, result, &error);
251 if (!js_result) {
252 g_warning("Error loading %s: %s", (const gchar*) loaded_library->data, error->message);
253 g_error_free(error);
254 g_object_unref(self);
255 /* Stop loading view, most likely resulting in a blank page */
256 return;
257 }
258 webkit_javascript_result_unref(js_result);
259
260 priv->js_libs_to_load = g_list_remove(priv->js_libs_to_load, loaded_library->data);
261
262 if(g_list_length(priv->js_libs_to_load) > 0)
263 {
264 /* keep loading... */
265 webkit_web_view_run_javascript_from_gresource(
266 webview_chat,
267 (const gchar*) g_list_first(priv->js_libs_to_load)->data,
268 NULL,
269 (GAsyncReadyCallback) javascript_library_loaded,
270 self
271 );
272 }
273 else
274 {
275 priv->js_libs_loaded = TRUE;
276 g_signal_emit(G_OBJECT(self), webkit_chat_container_signals[READY], 0);
277
278 /* The view could now be deleted without causing a crash */
279 g_object_unref(self);
280 }
281}
282
283static void
284load_javascript_libs(WebKitWebView *webview_chat,
285 WebKitChatContainer* self)
286{
287 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
288
289 /* Create the list of libraries to load */
290 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/jquery.js");
291 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify.js");
292 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify-string.js");
293 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify-html.js");
294 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify-jquery.js");
295
296 /* ref the chat view so that its not destroyed while we load
297 * we will unref in javascript_library_loaded
298 */
299 g_object_ref(self);
300
301 /* start loading */
302 webkit_web_view_run_javascript_from_gresource(
303 WEBKIT_WEB_VIEW(webview_chat),
304 (const gchar*) g_list_first(priv->js_libs_to_load)->data,
305 NULL,
306 (GAsyncReadyCallback) javascript_library_loaded,
307 self
308 );
309}
310
311static void
312webview_chat_load_changed(WebKitWebView *webview_chat,
313 WebKitLoadEvent load_event,
314 WebKitChatContainer* self)
315{
316 switch (load_event) {
317 case WEBKIT_LOAD_REDIRECTED:
318 {
319 g_warning("webview_chat load is being redirected, this should not happen");
320 }
321 case WEBKIT_LOAD_STARTED:
322 case WEBKIT_LOAD_COMMITTED:
323 {
324 break;
325 }
326 case WEBKIT_LOAD_FINISHED:
327 {
328 load_javascript_libs(webview_chat, self);
329 //TODO: disconnect? It shouldn't happen more than once
330 break;
331 }
332 }
333}
334
335static void
336build_view(WebKitChatContainer *view)
337{
338 g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(view));
339 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
340
341
342 priv->chatview_debug = FALSE;
343 auto ring_chatview_debug = g_getenv("RING_CHATVIEW_DEBUG");
344 if (ring_chatview_debug || g_strcmp0(ring_chatview_debug, "true") == 0)
345 {
346 priv->chatview_debug = TRUE;
347 }
348
aviau32b8dd62016-11-02 00:36:57 -0400349 /* Prepare WebKitUserContentManager */
350 WebKitUserContentManager* webkit_content_manager = webkit_user_content_manager_new();
351
352 WebKitUserStyleSheet* chatview_style_sheet = webkit_user_style_sheet_new(
353 (gchar*) g_bytes_get_data(
354 g_resources_lookup_data(
355 "/cx/ring/RingGnome/chatview.css",
356 G_RESOURCE_LOOKUP_FLAGS_NONE,
357 NULL
358 ),
359 NULL
360 ),
361 WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
362 WEBKIT_USER_STYLE_LEVEL_USER,
363 NULL,
364 NULL
365 );
366 webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);
367
368 /* Prepare WebKitSettings */
aviau039001d2016-09-29 16:39:05 -0400369 WebKitSettings* webkit_settings = webkit_settings_new_with_settings(
370 "enable-javascript", TRUE,
371 "enable-developer-extras", priv->chatview_debug,
372 "enable-java", FALSE,
373 "enable-plugins", FALSE,
374 "enable-site-specific-quirks", FALSE,
375 "enable-smooth-scrolling", TRUE,
376 NULL
377 );
aviau32b8dd62016-11-02 00:36:57 -0400378
379 /* Create the WebKitWebView */
380 priv->webview_chat = GTK_WIDGET(
381 webkit_web_view_new_with_user_content_manager(
382 webkit_content_manager
383 )
384 );
385
386 gtk_container_add(GTK_CONTAINER(priv->box_webview_chat), priv->webview_chat);
387 gtk_widget_show(priv->webview_chat);
388 gtk_widget_set_vexpand(GTK_WIDGET(priv->webview_chat), TRUE);
389 gtk_widget_set_hexpand(GTK_WIDGET(priv->webview_chat), TRUE);
390
391 /* Set the WebKitSettings */
aviau039001d2016-09-29 16:39:05 -0400392 webkit_web_view_set_settings(WEBKIT_WEB_VIEW(priv->webview_chat), webkit_settings);
393
394 g_signal_connect(priv->webview_chat, "load-changed", G_CALLBACK(webview_chat_load_changed), view);
395 g_signal_connect_swapped(priv->webview_chat, "context-menu", G_CALLBACK(webview_chat_context_menu), view);
396#if HAVE_WEBKIT2GTK4
397 g_signal_connect(priv->webview_chat, "decide-policy", G_CALLBACK(webview_chat_decide_policy), view);
398#endif
399
400 GBytes* chatview_bytes = g_resources_lookup_data(
401 "/cx/ring/RingGnome/chatview.html",
402 G_RESOURCE_LOOKUP_FLAGS_NONE,
403 NULL
404 );
405
406 webkit_web_view_load_html(
407 WEBKIT_WEB_VIEW(priv->webview_chat),
408 (gchar*) g_bytes_get_data(chatview_bytes, NULL),
409 NULL
410 );
411
412 /* Now we wait for the load-changed event, before we
413 * start loading javascript libraries */
414}
415
416GtkWidget *
417webkit_chat_container_new()
418{
419 gpointer view = g_object_new(WEBKIT_CHAT_CONTAINER_TYPE, NULL);
420
421 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
422 priv->js_libs_loaded = FALSE;
423
424 build_view(WEBKIT_CHAT_CONTAINER(view));
425
426 return (GtkWidget *)view;
427}
428
429void
aviaufc213552016-11-01 12:39:39 -0400430webkit_chat_container_clear_sender_images(WebKitChatContainer *view)
431{
432 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
433
434 webkit_web_view_run_javascript(
435 WEBKIT_WEB_VIEW(priv->webview_chat),
436 "ring.chatview.clearSenderImages()",
437 NULL,
438 NULL,
439 NULL
440 );
441}
442
443void
aviau039001d2016-09-29 16:39:05 -0400444webkit_chat_container_clear(WebKitChatContainer *view)
445{
446 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
447
448 webkit_web_view_run_javascript(
449 WEBKIT_WEB_VIEW(priv->webview_chat),
450 "ring.chatview.clearMessages()",
451 NULL,
452 NULL,
453 NULL
454 );
455
aviaufc213552016-11-01 12:39:39 -0400456 webkit_chat_container_clear_sender_images(view);
aviau039001d2016-09-29 16:39:05 -0400457}
458
459void
460webkit_chat_container_print_new_message(WebKitChatContainer *view, const QModelIndex &idx)
461{
462 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
463
464 auto message_object = message_index_to_json_message_object(idx).toUtf8().constData();
465 gchar* function_call = g_strdup_printf("ring.chatview.addMessage(%s);", message_object);
466 webkit_web_view_run_javascript(
467 WEBKIT_WEB_VIEW(priv->webview_chat),
468 function_call,
469 NULL,
470 NULL,
471 NULL
472 );
473 g_free(function_call);
474}
475
476void
477webkit_chat_container_update_message(WebKitChatContainer *view, const QModelIndex &idx)
478{
479 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
480
481 auto message_object = message_index_to_json_message_object(idx).toUtf8().constData();
482 gchar* function_call = g_strdup_printf("ring.chatview.updateMessage(%s);", message_object);
483 webkit_web_view_run_javascript(
484 WEBKIT_WEB_VIEW(priv->webview_chat),
485 function_call,
486 NULL,
487 NULL,
488 NULL
489 );
490 g_free(function_call);
491}
492
493void
aviaufc213552016-11-01 12:39:39 -0400494webkit_chat_container_set_sender_image(WebKitChatContainer *view, ContactMethod *sender_contact_method, QVariant sender_image)
aviau039001d2016-09-29 16:39:05 -0400495{
496 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
497
aviaufc213552016-11-01 12:39:39 -0400498 /* The sender_contact_method should be set to nullptr if the sender is self */
499 QString sender_contact_method_str;
500 if (sender_contact_method)
501 {
aviauac74ac32016-11-03 17:25:11 -0400502 sender_contact_method_str = QString().sprintf("%p", sender_contact_method);
aviaufc213552016-11-01 12:39:39 -0400503 }
504 else
505 {
506 sender_contact_method_str = "self";
507 }
508
aviau039001d2016-09-29 16:39:05 -0400509 auto sender_image_base64 = (QString) GlobalInstances::pixmapManipulator().toByteArray(sender_image).toBase64();
510
511 QJsonObject set_sender_image_object = QJsonObject();
aviaufc213552016-11-01 12:39:39 -0400512 set_sender_image_object.insert("sender_contact_method", QJsonValue(sender_contact_method_str));
aviau039001d2016-09-29 16:39:05 -0400513 set_sender_image_object.insert("sender_image", QJsonValue(sender_image_base64));
514
515 auto set_sender_image_object_string = QString(QJsonDocument(set_sender_image_object).toJson(QJsonDocument::Compact)).toUtf8().constData();
516
517 gchar* function_call = g_strdup_printf("ring.chatview.setSenderImage(%s);", set_sender_image_object_string);
518 webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(priv->webview_chat), function_call, NULL, NULL, NULL);
519 g_free(function_call);
520}
521
522gboolean
523webkit_chat_container_is_ready(WebKitChatContainer *view)
524{
525 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
526 return priv->js_libs_loaded;
527}