blob: e3ee1a8baa45ecabe4867b7481b74e435692eaf4 [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>
aviau039001d2016-09-29 16:39:05 -040024#include <webkit2/webkit2.h>
25
26// Qt
27#include <QtCore/QJsonValue>
28#include <QtCore/QJsonObject>
29#include <QtCore/QJsonDocument>
30
31// LRC
32#include <media/textrecording.h>
33#include <globalinstances.h>
aviaufc213552016-11-01 12:39:39 -040034#include <contactmethod.h>
aviau039001d2016-09-29 16:39:05 -040035
36// Ring Client
37#include "native/pixbufmanipulator.h"
38#include "config.h"
39
40struct _WebKitChatContainer
41{
42 GtkBox parent;
43};
44
45struct _WebKitChatContainerClass
46{
47 GtkBoxClass parent_class;
48};
49
50typedef struct _WebKitChatContainerPrivate WebKitChatContainerPrivate;
51
52struct _WebKitChatContainerPrivate
53{
54 GtkWidget* webview_chat;
aviau32b8dd62016-11-02 00:36:57 -040055 GtkWidget* box_webview_chat;
aviau039001d2016-09-29 16:39:05 -040056
57 bool chatview_debug;
58
59 /* Array of javascript libraries to load. Used during initialization */
60 GList* js_libs_to_load;
61 gboolean js_libs_loaded;
62};
63
64G_DEFINE_TYPE_WITH_PRIVATE(WebKitChatContainer, webkit_chat_container, GTK_TYPE_BOX);
65
66#define WEBKIT_CHAT_CONTAINER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), WEBKIT_CHAT_CONTAINER_TYPE, WebKitChatContainerPrivate))
67
68/* signals */
69enum {
70 READY,
71 LAST_SIGNAL
72};
73
74static guint webkit_chat_container_signals[LAST_SIGNAL] = { 0 };
75
76static void
77webkit_chat_container_dispose(GObject *object)
78{
79 G_OBJECT_CLASS(webkit_chat_container_parent_class)->dispose(object);
80}
81
82static void
83webkit_chat_container_init(WebKitChatContainer *view)
84{
85 gtk_widget_init_template(GTK_WIDGET(view));
86}
87
88static void
89webkit_chat_container_class_init(WebKitChatContainerClass *klass)
90{
91 G_OBJECT_CLASS(klass)->dispose = webkit_chat_container_dispose;
92
aviau039001d2016-09-29 16:39:05 -040093 gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
94 "/cx/ring/RingGnome/webkitchatcontainer.ui");
95
aviau32b8dd62016-11-02 00:36:57 -040096 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), WebKitChatContainer, box_webview_chat);
aviau039001d2016-09-29 16:39:05 -040097
98 /* add signals */
99 webkit_chat_container_signals[READY] = g_signal_new("ready",
100 G_TYPE_FROM_CLASS(klass),
101 (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
102 0,
103 nullptr,
104 nullptr,
105 g_cclosure_marshal_VOID__VOID,
106 G_TYPE_NONE, 0);
107}
108
109static gboolean
110webview_chat_context_menu(WebKitChatContainer *self)
111{
112 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
113 return !priv->chatview_debug;
114}
115
116QString
117message_index_to_json_message_object(const QModelIndex &idx)
118{
119 auto message = idx.data().value<QString>();
120 auto sender = idx.data(static_cast<int>(Media::TextRecording::Role::AuthorDisplayname)).value<QString>();
aviaufc213552016-11-01 12:39:39 -0400121 auto sender_contact_method = idx.data(static_cast<int>(Media::TextRecording::Role::ContactMethod)).value<ContactMethod*>();
aviau039001d2016-09-29 16:39:05 -0400122 auto timestamp = idx.data(static_cast<int>(Media::TextRecording::Role::Timestamp)).value<time_t>();
123 auto direction = idx.data(static_cast<int>(Media::TextRecording::Role::Direction)).value<Media::Media::Direction>();
124 auto message_id = idx.row();
125
aviaufc213552016-11-01 12:39:39 -0400126 QString sender_contact_method_str;
127 if(direction == Media::Media::Direction::IN)
128 {
129 sender_contact_method_str = QString(g_strdup_printf("%p", sender_contact_method));
130 }
131 else
132 {
133 sender_contact_method_str = "self";
134 }
135
aviau039001d2016-09-29 16:39:05 -0400136 QJsonObject message_object = QJsonObject();
137 message_object.insert("text", QJsonValue(message));
138 message_object.insert("id", QJsonValue(QString().setNum(message_id)));
139 message_object.insert("sender", QJsonValue(sender));
aviaufc213552016-11-01 12:39:39 -0400140 message_object.insert("sender_contact_method", QJsonValue(sender_contact_method_str));
aviau039001d2016-09-29 16:39:05 -0400141 message_object.insert("timestamp", QJsonValue((int) timestamp));
142 message_object.insert("direction", QJsonValue((direction == Media::Media::Direction::IN) ? "in" : "out"));
143
144 switch(idx.data(static_cast<int>(Media::TextRecording::Role::DeliveryStatus)).value<Media::TextRecording::Status>())
145 {
146 case Media::TextRecording::Status::FAILURE:
147 {
148 message_object.insert("delivery_status", QJsonValue("failure"));
149 break;
150 }
151 case Media::TextRecording::Status::COUNT__:
152 {
153 message_object.insert("delivery_status", QJsonValue("count__"));
154 break;
155 }
156 case Media::TextRecording::Status::SENDING:
157 {
158 message_object.insert("delivery_status", QJsonValue("sending"));
159 break;
160 }
161 case Media::TextRecording::Status::UNKNOWN:
162 {
163 message_object.insert("delivery_status", QJsonValue("unknown"));
164 break;
165 }
166 case Media::TextRecording::Status::READ:
167 {
168 message_object.insert("delivery_status", QJsonValue("read"));
169 break;
170 }
171 case Media::TextRecording::Status::SENT:
172 {
173 message_object.insert("delivery_status", QJsonValue("sent"));
174 break;
175 }
176 }
177
178 return QString(QJsonDocument(message_object).toJson(QJsonDocument::Compact));
179}
180
Stepan Salenikovichb52dbc52016-12-27 14:05:39 -0500181#if WEBKIT_CHECK_VERSION(2, 6, 0)
aviau039001d2016-09-29 16:39:05 -0400182static gboolean
183webview_chat_decide_policy (G_GNUC_UNUSED WebKitWebView *web_view,
184 WebKitPolicyDecision *decision,
185 WebKitPolicyDecisionType type)
186{
187 switch (type)
188 {
189 case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
190 case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
191 {
192 WebKitNavigationPolicyDecision* navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
193 WebKitNavigationAction* navigation_action = webkit_navigation_policy_decision_get_navigation_action(navigation_decision);
194 WebKitNavigationType navigation_type = webkit_navigation_action_get_navigation_type(navigation_action);
195
196 switch (navigation_type)
197 {
198 case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
199 case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
200 case WEBKIT_NAVIGATION_TYPE_RELOAD:
201 case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
202 case WEBKIT_NAVIGATION_TYPE_OTHER:
203 {
204 /* make no decision */
205 return FALSE;
206
207 }
208 case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
209 {
210 webkit_policy_decision_ignore(decision);
211
212 WebKitURIRequest* uri_request = webkit_navigation_action_get_request(navigation_action);
213 const gchar* uri = webkit_uri_request_get_uri(uri_request);
214
215 gtk_show_uri(NULL, uri, GDK_CURRENT_TIME, NULL);
216 }
217 }
218
219 webkit_policy_decision_ignore(decision);
220 break;
221 }
222 case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
223 {
224 //WebKitResponsePolicyDecision *response = WEBKIT_RESPONSE_POLICY_DECISION (decision);
225 //break;
226 return FALSE;
227 }
228 default:
229 {
230 /* Making no decision results in webkit_policy_decision_use(). */
231 return FALSE;
232 }
233 }
234 return TRUE;
235}
236#endif
237
238static void
239javascript_library_loaded(WebKitWebView *webview_chat,
240 GAsyncResult *result,
241 WebKitChatContainer* self)
242{
243 g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(self));
244 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
245
246 auto loaded_library = g_list_first(priv->js_libs_to_load);
247
248 GError *error = NULL;
249 WebKitJavascriptResult* js_result = webkit_web_view_run_javascript_from_gresource_finish(webview_chat, result, &error);
250 if (!js_result) {
251 g_warning("Error loading %s: %s", (const gchar*) loaded_library->data, error->message);
252 g_error_free(error);
253 g_object_unref(self);
254 /* Stop loading view, most likely resulting in a blank page */
255 return;
256 }
257 webkit_javascript_result_unref(js_result);
258
259 priv->js_libs_to_load = g_list_remove(priv->js_libs_to_load, loaded_library->data);
260
261 if(g_list_length(priv->js_libs_to_load) > 0)
262 {
263 /* keep loading... */
264 webkit_web_view_run_javascript_from_gresource(
265 webview_chat,
266 (const gchar*) g_list_first(priv->js_libs_to_load)->data,
267 NULL,
268 (GAsyncReadyCallback) javascript_library_loaded,
269 self
270 );
271 }
272 else
273 {
274 priv->js_libs_loaded = TRUE;
275 g_signal_emit(G_OBJECT(self), webkit_chat_container_signals[READY], 0);
276
277 /* The view could now be deleted without causing a crash */
278 g_object_unref(self);
279 }
280}
281
282static void
283load_javascript_libs(WebKitWebView *webview_chat,
284 WebKitChatContainer* self)
285{
286 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(self);
287
288 /* Create the list of libraries to load */
aviau039001d2016-09-29 16:39:05 -0400289 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify.js");
290 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify-string.js");
291 priv->js_libs_to_load = g_list_append(priv->js_libs_to_load, (gchar*) "/cx/ring/RingGnome/linkify-html.js");
aviau039001d2016-09-29 16:39:05 -0400292
293 /* ref the chat view so that its not destroyed while we load
294 * we will unref in javascript_library_loaded
295 */
296 g_object_ref(self);
297
298 /* start loading */
299 webkit_web_view_run_javascript_from_gresource(
300 WEBKIT_WEB_VIEW(webview_chat),
301 (const gchar*) g_list_first(priv->js_libs_to_load)->data,
302 NULL,
303 (GAsyncReadyCallback) javascript_library_loaded,
304 self
305 );
306}
307
308static void
309webview_chat_load_changed(WebKitWebView *webview_chat,
310 WebKitLoadEvent load_event,
311 WebKitChatContainer* self)
312{
313 switch (load_event) {
314 case WEBKIT_LOAD_REDIRECTED:
315 {
316 g_warning("webview_chat load is being redirected, this should not happen");
317 }
318 case WEBKIT_LOAD_STARTED:
319 case WEBKIT_LOAD_COMMITTED:
320 {
321 break;
322 }
323 case WEBKIT_LOAD_FINISHED:
324 {
325 load_javascript_libs(webview_chat, self);
326 //TODO: disconnect? It shouldn't happen more than once
327 break;
328 }
329 }
330}
331
332static void
333build_view(WebKitChatContainer *view)
334{
335 g_return_if_fail(IS_WEBKIT_CHAT_CONTAINER(view));
336 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
337
338
339 priv->chatview_debug = FALSE;
340 auto ring_chatview_debug = g_getenv("RING_CHATVIEW_DEBUG");
341 if (ring_chatview_debug || g_strcmp0(ring_chatview_debug, "true") == 0)
342 {
343 priv->chatview_debug = TRUE;
344 }
345
aviau32b8dd62016-11-02 00:36:57 -0400346 /* Prepare WebKitUserContentManager */
347 WebKitUserContentManager* webkit_content_manager = webkit_user_content_manager_new();
348
349 WebKitUserStyleSheet* chatview_style_sheet = webkit_user_style_sheet_new(
350 (gchar*) g_bytes_get_data(
351 g_resources_lookup_data(
352 "/cx/ring/RingGnome/chatview.css",
353 G_RESOURCE_LOOKUP_FLAGS_NONE,
354 NULL
355 ),
356 NULL
357 ),
358 WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
359 WEBKIT_USER_STYLE_LEVEL_USER,
360 NULL,
361 NULL
362 );
363 webkit_user_content_manager_add_style_sheet(webkit_content_manager, chatview_style_sheet);
364
365 /* Prepare WebKitSettings */
aviau039001d2016-09-29 16:39:05 -0400366 WebKitSettings* webkit_settings = webkit_settings_new_with_settings(
367 "enable-javascript", TRUE,
368 "enable-developer-extras", priv->chatview_debug,
369 "enable-java", FALSE,
370 "enable-plugins", FALSE,
371 "enable-site-specific-quirks", FALSE,
372 "enable-smooth-scrolling", TRUE,
373 NULL
374 );
aviau32b8dd62016-11-02 00:36:57 -0400375
376 /* Create the WebKitWebView */
377 priv->webview_chat = GTK_WIDGET(
378 webkit_web_view_new_with_user_content_manager(
379 webkit_content_manager
380 )
381 );
382
383 gtk_container_add(GTK_CONTAINER(priv->box_webview_chat), priv->webview_chat);
384 gtk_widget_show(priv->webview_chat);
385 gtk_widget_set_vexpand(GTK_WIDGET(priv->webview_chat), TRUE);
386 gtk_widget_set_hexpand(GTK_WIDGET(priv->webview_chat), TRUE);
387
388 /* Set the WebKitSettings */
aviau039001d2016-09-29 16:39:05 -0400389 webkit_web_view_set_settings(WEBKIT_WEB_VIEW(priv->webview_chat), webkit_settings);
390
391 g_signal_connect(priv->webview_chat, "load-changed", G_CALLBACK(webview_chat_load_changed), view);
392 g_signal_connect_swapped(priv->webview_chat, "context-menu", G_CALLBACK(webview_chat_context_menu), view);
Stepan Salenikovichb52dbc52016-12-27 14:05:39 -0500393#if WEBKIT_CHECK_VERSION(2, 6, 0)
aviau039001d2016-09-29 16:39:05 -0400394 g_signal_connect(priv->webview_chat, "decide-policy", G_CALLBACK(webview_chat_decide_policy), view);
395#endif
396
397 GBytes* chatview_bytes = g_resources_lookup_data(
398 "/cx/ring/RingGnome/chatview.html",
399 G_RESOURCE_LOOKUP_FLAGS_NONE,
400 NULL
401 );
402
403 webkit_web_view_load_html(
404 WEBKIT_WEB_VIEW(priv->webview_chat),
405 (gchar*) g_bytes_get_data(chatview_bytes, NULL),
406 NULL
407 );
408
409 /* Now we wait for the load-changed event, before we
410 * start loading javascript libraries */
411}
412
413GtkWidget *
414webkit_chat_container_new()
415{
416 gpointer view = g_object_new(WEBKIT_CHAT_CONTAINER_TYPE, NULL);
417
418 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
419 priv->js_libs_loaded = FALSE;
420
421 build_view(WEBKIT_CHAT_CONTAINER(view));
422
423 return (GtkWidget *)view;
424}
425
426void
aviaufc213552016-11-01 12:39:39 -0400427webkit_chat_container_clear_sender_images(WebKitChatContainer *view)
428{
429 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
430
431 webkit_web_view_run_javascript(
432 WEBKIT_WEB_VIEW(priv->webview_chat),
433 "ring.chatview.clearSenderImages()",
434 NULL,
435 NULL,
436 NULL
437 );
438}
439
440void
aviau039001d2016-09-29 16:39:05 -0400441webkit_chat_container_clear(WebKitChatContainer *view)
442{
443 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
444
445 webkit_web_view_run_javascript(
446 WEBKIT_WEB_VIEW(priv->webview_chat),
447 "ring.chatview.clearMessages()",
448 NULL,
449 NULL,
450 NULL
451 );
452
aviaufc213552016-11-01 12:39:39 -0400453 webkit_chat_container_clear_sender_images(view);
aviau039001d2016-09-29 16:39:05 -0400454}
455
456void
457webkit_chat_container_print_new_message(WebKitChatContainer *view, const QModelIndex &idx)
458{
459 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
460
461 auto message_object = message_index_to_json_message_object(idx).toUtf8().constData();
462 gchar* function_call = g_strdup_printf("ring.chatview.addMessage(%s);", message_object);
463 webkit_web_view_run_javascript(
464 WEBKIT_WEB_VIEW(priv->webview_chat),
465 function_call,
466 NULL,
467 NULL,
468 NULL
469 );
470 g_free(function_call);
471}
472
473void
474webkit_chat_container_update_message(WebKitChatContainer *view, const QModelIndex &idx)
475{
476 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
477
478 auto message_object = message_index_to_json_message_object(idx).toUtf8().constData();
479 gchar* function_call = g_strdup_printf("ring.chatview.updateMessage(%s);", message_object);
480 webkit_web_view_run_javascript(
481 WEBKIT_WEB_VIEW(priv->webview_chat),
482 function_call,
483 NULL,
484 NULL,
485 NULL
486 );
487 g_free(function_call);
488}
489
490void
aviaufc213552016-11-01 12:39:39 -0400491webkit_chat_container_set_sender_image(WebKitChatContainer *view, ContactMethod *sender_contact_method, QVariant sender_image)
aviau039001d2016-09-29 16:39:05 -0400492{
493 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
494
aviaufc213552016-11-01 12:39:39 -0400495 /* The sender_contact_method should be set to nullptr if the sender is self */
496 QString sender_contact_method_str;
497 if (sender_contact_method)
498 {
aviauac74ac32016-11-03 17:25:11 -0400499 sender_contact_method_str = QString().sprintf("%p", sender_contact_method);
aviaufc213552016-11-01 12:39:39 -0400500 }
501 else
502 {
503 sender_contact_method_str = "self";
504 }
505
aviau039001d2016-09-29 16:39:05 -0400506 auto sender_image_base64 = (QString) GlobalInstances::pixmapManipulator().toByteArray(sender_image).toBase64();
507
508 QJsonObject set_sender_image_object = QJsonObject();
aviaufc213552016-11-01 12:39:39 -0400509 set_sender_image_object.insert("sender_contact_method", QJsonValue(sender_contact_method_str));
aviau039001d2016-09-29 16:39:05 -0400510 set_sender_image_object.insert("sender_image", QJsonValue(sender_image_base64));
511
Stepan Salenikovich407dc412016-11-29 11:01:31 -0500512 auto set_sender_image_object_string = QString(QJsonDocument(set_sender_image_object).toJson(QJsonDocument::Compact));
aviau039001d2016-09-29 16:39:05 -0400513
Stepan Salenikovich407dc412016-11-29 11:01:31 -0500514 gchar* function_call = g_strdup_printf("ring.chatview.setSenderImage(%s);", set_sender_image_object_string.toUtf8().constData());
aviau039001d2016-09-29 16:39:05 -0400515 webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(priv->webview_chat), function_call, NULL, NULL, NULL);
516 g_free(function_call);
517}
518
519gboolean
520webkit_chat_container_is_ready(WebKitChatContainer *view)
521{
522 WebKitChatContainerPrivate *priv = WEBKIT_CHAT_CONTAINER_GET_PRIVATE(view);
523 return priv->js_libs_loaded;
524}