From 3b71cdad64138d0c20a36d5e8765b66ab7c9a8c1 Mon Sep 17 00:00:00 2001
From: Yang Wang <yang.wang@savoirfairelinux.com>
Date: Thu, 10 Oct 2019 15:34:43 -0400
Subject: [PATCH] chatview: initial commit for chatview folder created in lrc

Change-Id: I0b960170ce4a89f162f7a804bdbca229757db0d7
---
 messagewebview.cpp                  |   18 +-
 messagewebview.h                    |    2 +-
 ressources.qrc                      |   15 +-
 ring-client-windows.vcxproj         |   15 +-
 ring-client-windows.vcxproj.filters |   19 +-
 web/chatview.css                    | 1182 ------------------
 web/chatview.html                   |   64 -
 web/chatview.js                     | 1794 ---------------------------
 web/linkify-html.js                 |  827 ------------
 web/linkify-string.js               |  118 --
 web/linkify.js                      | 1271 -------------------
 web/qwebchannel.js                  |  427 -------
 12 files changed, 38 insertions(+), 5714 deletions(-)
 delete mode 100644 web/chatview.css
 delete mode 100644 web/chatview.html
 delete mode 100644 web/chatview.js
 delete mode 100644 web/linkify-html.js
 delete mode 100644 web/linkify-string.js
 delete mode 100644 web/linkify.js
 delete mode 100644 web/qwebchannel.js

diff --git a/messagewebview.cpp b/messagewebview.cpp
index 8b5ee1b..71a89e3 100644
--- a/messagewebview.cpp
+++ b/messagewebview.cpp
@@ -92,6 +92,7 @@ MessageWebView::MessageWebView(QWidget *parent)
     webChannel_ = new QWebChannel(page());
     webChannel_->registerObject(QStringLiteral("jsbridge"), jsBridge_);
     page()->setWebChannel(webChannel_);
+    page()->profile()->setHttpUserAgent("jami-windows");
 
     connect(this, &QWebEngineView::renderProcessTerminated,
         [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) {
@@ -239,20 +240,21 @@ void MessageWebView::runJsText()
 
 void MessageWebView::buildView()
 {
-    auto html = Utils::QByteArrayFromFile(":/web/chatview.html");
-    page()->setHtml(html, QUrl(":/web/chatview.html"));
+    auto html = Utils::QByteArrayFromFile(":/chatview.html");
+    page()->setHtml(html, QUrl(":/chatview.html"));
     connect(this, &QWebEngineView::loadFinished, this, &MessageWebView::slotLoadFinished);
 }
 
 void
 MessageWebView::slotLoadFinished()
 {
-    insertStyleSheet("chatcss", Utils::QByteArrayFromFile(":/web/chatview.css"));
-    page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify.js"), QWebEngineScript::MainWorld);
-    page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-html.js"), QWebEngineScript::MainWorld);
-    page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-string.js"), QWebEngineScript::MainWorld);
-    page()->runJavaScript(Utils::QByteArrayFromFile(":/web/qwebchannel.js"), QWebEngineScript::MainWorld);
-    page()->runJavaScript(Utils::QByteArrayFromFile(":/web/chatview.js"), QWebEngineScript::MainWorld);
+    insertStyleSheet("chatcss", Utils::QByteArrayFromFile(":/chatview.css"));
+    insertStyleSheet("chatwin",Utils::QByteArrayFromFile(":/chatview-windows.css"));
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify.js"), QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-html.js"), QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-string.js"), QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/qwebchannel.js"), QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/chatview.js"), QWebEngineScript::MainWorld);
 }
 
 void
diff --git a/messagewebview.h b/messagewebview.h
index 1dc8095..84691d4 100644
--- a/messagewebview.h
+++ b/messagewebview.h
@@ -1,5 +1,5 @@
 /***************************************************************************
- * Copyright (C) 2019-2019 by Savoir-faire Linux                                *
+ * Copyright (C) 2019-2019 by Savoir-faire Linux                           *
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
  *                                                                         *
  * This program is free software; you can redistribute it and/or modify    *
diff --git a/ressources.qrc b/ressources.qrc
index bf74d10..31b5bf0 100644
--- a/ressources.qrc
+++ b/ressources.qrc
@@ -67,13 +67,14 @@
     <file>images/icons/round-remove_circle-24px.svg</file>
     <file>images/icons/round-settings-24px.svg</file>
     <file>images/icons/round-undo-24px.svg</file>
-    <file>web/chatview.css</file>
-    <file>web/chatview.html</file>
-    <file>web/chatview.js</file>
-    <file>web/linkify.js</file>
-    <file>web/linkify-html.js</file>
-    <file>web/linkify-string.js</file>
-    <file>web/qwebchannel.js</file>
+    <file alias="chatview.css">../lrc/src/web-chatview/chatview.css</file>
+    <file alias="chatview.html">../lrc/src/web-chatview/chatview.html</file>
+    <file alias="chatview.js">../lrc/src/web-chatview/chatview.js</file>
+    <file alias="linkify.js">../lrc/src/web-chatview/linkify.js</file>
+    <file alias="linkify-html.js">../lrc/src/web-chatview/linkify-html.js</file>
+    <file alias="linkify-string.js">../lrc/src/web-chatview/linkify-string.js</file>
+    <file alias="qwebchannel.js">../lrc/src/web-chatview/qwebchannel.js</file>
+    <file alias="chatview-windows.css">../lrc/src/web-chatview/chatview-windows.css</file>
     <file>images/icons/round-check_circle-24px.svg</file>
     <file>images/icons/round-error-24px.svg</file>
     <file>images/icons/round-save_alt-24px.svg</file>
diff --git a/ring-client-windows.vcxproj b/ring-client-windows.vcxproj
index bd8a47e..3f1ae4c 100644
--- a/ring-client-windows.vcxproj
+++ b/ring-client-windows.vcxproj
@@ -786,6 +786,14 @@ del /s /q $(OutDir)\Jami.exp</Command>
     <ClInclude Include="ui_videoview.h" />
   </ItemGroup>
   <ItemGroup>
+    <None Include="..\lrc\src\web-chatview\chatview-windows.css" />
+    <None Include="..\lrc\src\web-chatview\chatview.css" />
+    <None Include="..\lrc\src\web-chatview\chatview.html" />
+    <None Include="..\lrc\src\web-chatview\chatview.js" />
+    <None Include="..\lrc\src\web-chatview\linkify-html.js" />
+    <None Include="..\lrc\src\web-chatview\linkify-string.js" />
+    <None Include="..\lrc\src\web-chatview\linkify.js" />
+    <None Include="..\lrc\src\web-chatview\qwebchannel.js" />
     <None Include="translations\ring_client_windows.ts" />
     <None Include="translations\ring_client_windows_ar.ts" />
     <None Include="translations\ring_client_windows_bg.ts" />
@@ -838,13 +846,6 @@ del /s /q $(OutDir)\Jami.exp</Command>
     <None Include="translations\ring_client_windows_zh.ts" />
     <None Include="translations\ring_client_windows_zh_CN.ts" />
     <None Include="translations\ring_client_windows_zh_TW.ts" />
-    <None Include="web\chatview.css" />
-    <None Include="web\chatview.html" />
-    <None Include="web\chatview.js" />
-    <None Include="web\linkify-html.js" />
-    <None Include="web\linkify-string.js" />
-    <None Include="web\linkify.js" />
-    <None Include="web\qwebchannel.js" />
   </ItemGroup>
   <ItemGroup>
     <QtUic Include="aboutdialog.ui">
diff --git a/ring-client-windows.vcxproj.filters b/ring-client-windows.vcxproj.filters
index 906dc2f..a729d4e 100644
--- a/ring-client-windows.vcxproj.filters
+++ b/ring-client-windows.vcxproj.filters
@@ -618,25 +618,28 @@
     <None Include="translations\ring_client_windows_zh_TW.ts">
       <Filter>Translation Files</Filter>
     </None>
-    <None Include="web\chatview.css">
+    <None Include="..\lrc\src\web-chatview\chatview.css">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\chatview.html">
+    <None Include="..\lrc\src\web-chatview\chatview.html">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\linkify.js">
+    <None Include="..\lrc\src\web-chatview\chatview.js">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\linkify-html.js">
+    <None Include="..\lrc\src\web-chatview\chatview-windows.css">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\linkify-string.js">
+    <None Include="..\lrc\src\web-chatview\linkify.js">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\chatview.js">
+    <None Include="..\lrc\src\web-chatview\linkify-html.js">
       <Filter>Resource Files\web</Filter>
     </None>
-    <None Include="web\qwebchannel.js">
+    <None Include="..\lrc\src\web-chatview\linkify-string.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="..\lrc\src\web-chatview\qwebchannel.js">
       <Filter>Resource Files\web</Filter>
     </None>
   </ItemGroup>
@@ -876,4 +879,4 @@
       <Filter>Resource Files</Filter>
     </Image>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/web/chatview.css b/web/chatview.css
deleted file mode 100644
index 6866114..0000000
--- a/web/chatview.css
+++ /dev/null
@@ -1,1182 +0,0 @@
-/** Variable and font definitions */
-
-:root {
-    /* color definitions */
-    --jami-light-blue: rgba(59, 193, 211, 0.3);
-    --jami-dark-blue: #004e86;
-    --jami-green: #219d55;
-    --jami-green-hover: #1f8b4c;
-    --jami-red: #dc2719;
-    --jami-red-hover: #b02e2c;
-    /* main properties */
-    --bg-color: #ffffff;
-    /* navbar properties */
-    --navbar-height: 40px;
-    --navbar-padding-top: 8px;
-    --navbar-padding-bottom: var(--navbar-padding-top);
-    /* message bar properties */
-    --textarea-max-height: 150px;
-    --placeholder-text-color: #d3d3d3;
-    /* button properties */
-    --action-icon-color: #00;
-    --deactivated-icon-color: #bebebe;
-    --action-icon-hover-color: #ededed;
-    --action-critical-icon-hover-color: rgba(211, 77, 59, 0.3); /* complementary color of jami light blue */
-    --action-critical-icon-press-color: rgba(211, 77, 59, 0.5);
-    --action-critical-icon-color: #4E1300;
-    --non-action-icon-color: #212121;
-    --action-icon-press-color: rgba(212, 212, 212, 1.0);
-    --invite-hover-color: white;
-    /* hairline properties */
-    --hairline-color: #f0f0f0;
-    --hairline-thickness: 2px;
-}
-
-@font-face {
-    font-family: emoji;
-    /* Fonts for text outside emoji blocks */
-    src: local('Open sans'), local('Helvetica'), local('Segoe UI'), local('sans-serif');
-}
-
-@font-face {
-    font-family: emoji;
-    src: local('Noto Color Emoji'), local('Android Emoji'), local('Twitter Color Emoji');
-    /* Emoji unicode blocks */
-    unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF;
-}
-
-/** Body */
-
-body {
-    --messagebar-size: 57px;
-    margin: 0;
-    overflow: hidden;
-    background-color: var(--bg-color);
-    padding-bottom: var(--messagebar-size);
-    /* disable selection highlight because it looks very bad */
-    -webkit-user-select: text;
-
-}
-
-::-webkit-scrollbar-track {
-    background-color: var(--bg-color);
-}
-
-::-webkit-scrollbar {
-    width: 8px;
-    background-color: var(--bg-color);
-}
-
-::-webkit-scrollbar-thumb {
-    background-color: #f0f0f0;
-}
-
-/** Navbar */
-
-.navbar-wrapper {
-    /* on top, over everything and full width */
-    position: fixed;
-    left: 0;
-    right: 0;
-    z-index: 500;
-    top: 0;
-}
-
-#navbar {
-    background-color: var(--bg-color);
-    padding-right: 8px;
-    padding-left: 8px;
-    padding-top: var(--navbar-padding-top);
-    padding-bottom: var(--navbar-padding-bottom);
-    height: var(--navbar-height);
-    overflow: hidden;
-    align-items: center;
-    /* takes whole width */
-    left: 0;
-    right: 0;
-    /* hairline */
-    border-bottom: var(--hairline-thickness) solid var(--hairline-color);
-    display: flex;
-}
-
-.hiddenState {
-    /* Used to hide navbar and message bar */
-    display: none !important;
-}
-
-.svgicon {
-    display: block;
-    margin: auto;
-    height: 70%;
-}
-
-.nav-button {
-    width: 30px;
-    height: 30px;
-    margin: 8px;
-    padding: 2px;
-    display: flex;
-    cursor: pointer;
-    align-self: center;
-    border-radius: 16px;
-}
-
-    .nav-button.deactivated {
-        width: 30px;
-        height: 30px;
-        margin: 8px;
-        padding: 2px;
-        align-self: center;
-        display: flex;
-        border-radius: 16px;
-        cursor: auto;
-    }
-
-.action-button svg {
-    fill: var(--action-icon-color);
-}
-
-.action-button.deactivated svg {
-    fill: var(--deactivated-icon-color);
-}
-
-.non-action-button svg {
-    fill: var(--non-action-icon-color);
-}
-
-.non-action-button:hover, .action-button:hover {
-    background: var(--action-icon-hover-color);
-}
-
-.non-action-button:active, .action-button:active {
-    background: var(--action-icon-press-color);
-}
-
-.action-button.deactivated:hover, .action-button.deactivated:active {
-    background: none;
-}
-
-.action-critical-button svg {
-    fill: var(--action-critical-icon-color);
-}
-
-.action-critical-button:hover {
-    background: var(--action-critical-icon-hover-color);
-}
-
-.action-critical-button:active {
-    background: var(--action-critical-icon-press-color);
-}
-
-#callButtons {
-    display: flex;
-}
-
-#navbar #addBannedContactButton, #navbar #addToConversationsButton {
-    display: none;
-}
-
-#navbar.onBannedState #addToConvButton, #navbar.onBannedState #callButtons, #navbar.onBannedState #addToConversationsButton {
-    display: none;
-}
-
-#navbar.onBannedState #addBannedContactButton {
-    display: flex;
-}
-
-/** Invitation bar */
-
-#invitation {
-    visibility: hidden;
-    background: var(--bg-color);
-    position: absolute;
-    width: 100%;
-    padding-bottom: 10px;
-
-    /* hairline */
-    border-bottom: var(--hairline-thickness) solid var(--hairline-color);
-}
-
-    #invitation #invite_header {
-        margin: 10px;
-        list-style: none;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        /* enable selection (it is globally disabled in the body) */
-        -webkit-user-select: auto;
-    }
-
-#invitation .sender_image {
-  width: 50px;
-  height: 50px;
-}
-
-#invitation #actions {
-  margin-right: auto;
-  margin-left: auto;
-  list-style: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  /* enable selection (it is globally disabled in the body) */
-  -webkit-user-select: auto;
-}
-
-#invitation #actions div {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-right: 5px;
-    margin-left: 5px;
-}
-
-    #invitation #text {
-        text-align: center;
-        font: 11pt sans-serif;
-
-        /* enable selection (it is globally disabled in the body) */
-        -webkit-user-select: auto;
-    }
-
-#invite_contact_name {
-    font-weight: 500;
-}
-
-/** Messaging bar */
-#sendMessage {
-    background-color: var(--bg-color);
-    display: flex;
-    overflow: hidden;
-    padding: 4px;
-    align-items: center;
-    position: fixed;
-    left: 0;
-    right: 0;
-    z-index: 500;
-    bottom: 0;
-    /* hairline */
-    border-top: var(--hairline-thickness) solid var(--hairline-color);
-}
-
-#message {
-    font-family: emoji;
-    flex: 1;
-    background-color: var(--bg-color);
-    border: 0;
-    overflow-y: scroll;
-    color: black;
-    max-height: var(--textarea-max-height);
-    margin-right: 10px;
-    resize: none;
-    /* enable selection (it is globally disabled in the body) */
-    -webkit-user-select: auto;
-}
-
-    #message:focus,
-    #message.focus {
-        outline: none;
-    }
-
-#container[disabled] {
-    background-color: #ccc;
-}
-
-input[placeholder], [placeholder], *[placeholder] {
-    color: var(--placeholder-text-color);
-}
-
-/** Main chat view */
-
-#lazyloading-icon {
-    margin: auto;
-    margin-bottom: 10px;
-    margin-top: 5px;
-    vertical-align: center;
-    width: 30px;
-    display: flex;
-}
-
-#container {
-    position: relative;
-    height: 100%;
-    width: 100%;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    flex-direction: column;
-    /* When there are not enough messages to occupy full height of the
-       container, make sure they are displayed at the bottom, not at the
-       top. */
-    justify-content: flex-end;
-}
-
-a:link {
-    text-decoration: none;
-    color: #0e649b;
-    transition: all 0.2s ease-in-out;
-    border-bottom: dotted 1px;
-}
-
-a:hover {
-    border: 0;
-}
-
-#back_to_bottom_button_container {
-    position: absolute;
-    z-index: 1;
-    display: flex;
-    justify-content: center;
-    width: 100%;
-    height: 4em;
-    pointer-events: none;
-}
-
-#file_image_send_container {
-    visibility: hidden;
-    position: fixed;
-    bottom: var(--messagebar-size);
-    z-index: 1;
-    display: flex;
-    justify-content: flex-start;
-    left: 0;
-    right: 0;
-    height: 8em;
-    /*border-top-left-radius: 25px;
-    border-top-right-radius: 25px;*/
-    border: 2px solid lightgray;
-    padding: 20px;
-    border-bottom: none;
-    background-color: #cfdbdd;
-    overflow-x: overlay;
-}
-
-    #file_image_send_container::-webkit-scrollbar{
-        height: 10px;
-    }
-
-    #file_image_send_container::after {
-        /*Create the margins with pseudo-elements*/
-        /*to solve overflow:scroll and The Right Padding Problem*/
-        content: ' ';
-        min-width: 20px;
-    }
-
-.img_wrapper {
-    position: relative;
-    max-width: 65px;
-    min-width: 65px;
-    max-height: 80px;
-    border: 3px solid rgba(255,255,255,0);
-    padding: 30px;
-    border-radius: 20px;
-    background-color: #cfdbdd;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    margin: 5px;
-}
-
-/* Make the image responsive */
-    .img_wrapper img {
-        height: 118px;
-        min-width: 131px;
-        object-fit: cover;
-        border-radius: 20px;
-    }
-
-/* Style the button and place it at the top right corner of the image */
-    .img_wrapper .btn {
-        position: absolute;
-        color: #fff;
-        border: 1px solid #AEAEAE;
-        border-radius: 40%;
-        background: rgba(96,95,97,0.5);
-        font-size: 10px;
-        font-weight: bold;
-        top: -5%;
-        right: -5%;
-    }
-
-        .img_wrapper .btn:hover {
-            background-color: lightgray;
-        }
-
-        .img_wrapper .btn:focus {
-            outline: none;
-            color: black;
-        }
-
-.file_wrapper {
-    position: relative;
-    max-width: 65px;
-    min-width: 65px;
-    max-height: 80px;
-    border: 3px solid rgba(255,255,255,0);
-    padding: 30px;
-    border-radius: 20px;
-    background-color: white;
-    display: flex;
-    justify-content: flex-start;
-    align-items: center;
-    margin: 5px;
-    font-family: sans-serif;
-}
-    /* Style the button and place it at the top right corner of the image */
-    .file_wrapper .btn {
-        position: absolute;
-        color: #fff;
-        border: 1px solid #AEAEAE;
-        border-radius: 40%;
-        background: rgba(96,95,97,0.5);
-        font-size: 10px;
-        font-weight: bold;
-        top: -5%;
-        right: -5%;
-    }
-
-        .file_wrapper .btn:hover {
-            background-color: lightgray;
-        }
-
-        .file_wrapper .btn:focus {
-            outline: none;
-            color: black;
-        }
-
-    .file_wrapper .svg-icon {
-        position: absolute;
-        max-width: 2em;
-        max-height: 25px;
-        margin-right: 2em;
-        top: 8%;
-        left: 1px;
-    }
-
-        .file_wrapper .svg-icon path,
-        .file_wrapper .svg-icon polygon,
-        .file_wrapper .svg-icon rect {
-            fill: #000000;
-        }
-
-        .file_wrapper .svg-icon circle {
-            stroke: #4691f6;
-            stroke-width: 1;
-        }
-    .file_wrapper .fileinfo {
-        position: absolute;
-        top: 30%;
-        left: 7%;
-    }
-
-#back_to_bottom_button {
-    visibility: hidden;
-    margin: auto;
-    font: 0.875em emoji;
-    text-align: center;
-    width: 10em;
-    border-radius: 2em;
-    background-color: var(--jami-dark-blue);
-    color: #fff;
-    padding: 0.5em;
-    box-shadow: 2px 2px 4px black;
-    opacity: 1;
-    overflow: hidden;
-    white-space: nowrap;
-    transition: opacity .5s ease, width .2s ease, color .1s ease .2s;
-    pointer-events: all;
-}
-
-    #back_to_bottom_button:hover {
-        cursor: pointer;
-    }
-
-    #back_to_bottom_button.fade {
-        opacity: 0;
-        width: 1em;
-        color: transparent;
-        transition: .2s opacity ease .1s, .2s width ease .1s, color .1s ease;
-    }
-        #back_to_bottom_button.fade:hover {
-            cursor: context-menu;
-        }
-
-#messages {
-    position: relative;
-    z-index: 0;
-    width: 100%;
-    overflow: hidden;
-    height: auto;
-    padding-top: 0.5em;
-    opacity: 1;
-    transition: 0.5s opacity;
-}
-
-    #messages.fade {
-        opacity: 0;
-        transition: none;
-    }
-
-    #messages:hover {
-        overflow-y: overlay;
-    }
-
-.last_message {
-    /* The last message gets a bigger bottom padding so that it is not
-     "glued" to the message bar. */
-    padding-bottom: 1.5em !important;
-    margin-top: 4px;
-}
-
-/* General messages */
-
-.internal_mes_wrapper {
-    max-width: 70%;
-    margin: 0px 0 0 0;
-    display: flex;
-    flex-direction: column;
-    /* If a message is smaller (in width) than the timestamp, do not fill
-       full width and pack message at the left. */
-    align-items: flex-start;
-    align-content: flex-start;
-}
-
-.message_out > .internal_mes_wrapper {
-    /* If message is in the outgoing direction, pack at the right. */
-    align-items: flex-end;
-    align-content: flex-end;
-}
-
-.message_wrapper {
-    max-width: calc(100% - 2em);
-    border-radius: 10px;
-    padding: 0.5em 1em 0.5em 1em;
-    position: relative;
-    display: flex;
-    flex-direction: row;
-}
-
-.message_type_data_transfer .message_wrapper {
-    display: flex;
-    flex-direction: column;
-    padding: 0;
-    width: 450px;
-    max-width: none;
-}
-
-.transfer_info_wrapper {
-    display: flex;
-    flex-direction: row;
-}
-
-.message {
-    font: 0.875em emoji;
-    margin: 0;
-    display: flex;
-    justify-content: flex-start;
-    align-items: top;
-    overflow: hidden;
-    /* enable selection (it is globally disabled in the body) */
-    -webkit-user-select: auto;
-}
-
-.message_in {
-    padding-left: 25%;
-}
-
-.message_out {
-    padding-right: 25%;
-    /* Message sent by the user should be displayed at the right side of
-       the screen. */
-    flex-direction: row-reverse;
-}
-
-.message_delivery_status {
-    margin: 10px 10px;
-    color: #A0A0A0;
-}
-
-.message_sender {
-    display: none;
-}
-
-.sender_image, .invite_sender_image {
-    border-radius: 50%;
-    margin: 0px 10px 0px 10px;
-}
-
-div.last_message > span {
-    margin: 0px 10px 0px 10px;
-}
-
-.message_out .message_wrapper {
-    border-top-right-radius: 0;
-    transform-origin: top right;
-}
-
-.message_in .message_wrapper {
-    border-top-left-radius: 0;
-    transform-origin: top left;
-}
-
-.message_out .message_wrapper {
-    background-color: #cfd8dc;
-}
-
-.message_in .message_wrapper {
-    background-color: #cfebf5;
-}
-
-.message_in .sender_image,
-.message_out .sender_image {
-    visibility: hidden;
-}
-
-.message_in.last_of_sequence .sender_image,
-.message_in.single_message .sender_image {
-    visibility: visible;
-}
-
-.message_in.last_of_sequence .sender_image {
-    margin-top: 2px;
-}
-
-.message_in.middle_of_sequence .sender_image {
-    margin-top: 0px;
-}
-
-.generated_message.message_in .message_wrapper,
-.generated_message.message_out .message_wrapper {
-    background-color: transparent !important;
-    border-radius: 0px !important;
-}
-
-.single_message.message_in .message_wrapper,
-.single_message.message_out .message_wrapper {
-    border-radius: 20px 20px 20px 20px !important;
-}
-
-.last_of_sequence.message_in .message_wrapper {
-    border-radius: 4px 20px 20px 20px;
-}
-
-.first_of_sequence.message_in .message_wrapper {
-    border-radius: 20px 20px 20px 4px;
-}
-
-.middle_of_sequence.message_in .message_wrapper {
-    border-radius: 4px 20px 20px 5px;
-}
-
-.last_of_sequence.message_out .message_wrapper {
-    border-radius: 20px 4px 20px 20px;
-}
-
-.first_of_sequence.message_out .message_wrapper {
-    border-radius: 20px 20px 4px 20px;
-}
-
-.middle_of_sequence.message_out .message_wrapper {
-    border-radius: 20px 5px 4px 20px;
-}
-
-.middle_of_sequence .internal_mes_wrapper,
-.last_of_sequence .internal_mes_wrapper,
-.last_message .internal_mes_wrapper {
-    margin: 0px 0 0 0 !important;
-}
-
-.message_out .sender_image {
-    margin: 8px;
-}
-
-.first_of_sequence.message_out .internal_mes_wrapper,
-.single_message.message_out .internal_mes_wrapper {
-    margin: 0px 0 0 0 !important;
-}
-
-@-webkit-keyframes fade-in {
-    from {
-        opacity: 0;
-    }
-
-    to {
-        opacity: 1;
-    }
-}
-
-.sender_image_cell {
-    vertical-align: bottom;
-    min-width: 16px;
-}
-
-.message_in .sender_image_cell {
-    min-width: 56px;
-}
-
-.dummy_cell {
-    padding: 0;
-}
-
-.timestamp_cell {
-    padding: 0;
-    max-width: 0px;
-    overflow: visible;
-    white-space: nowrap;
-}
-
-.timestamp_cell_out {
-    padding: 0;
-    text-align: right;
-    direction: rtl;
-    max-width: 0px;
-    overflow: visible;
-    white-space: nowrap;
-}
-
-table {
-    border-collapse: collapse;
-    border-spacing: 0 0px;
-    margin: 0;
-    padding: 0;
-}
-
-.timestamp {
-    display: inline-flex;
-    justify-content: flex-start;
-    align-self: stretch;
-    color: #333;
-    font-size: 10px;
-    padding: 5px;
-}
-
-.timestamp_out {
-    flex-direction: row-reverse;
-}
-
-.timestamp_action {
-    margin: auto;
-    padding: 0;
-    vertical-align: center;
-    opacity: 0;
-    transition: visibility 0.3s linear, opacity 0.3s linear;
-}
-
-.message_type_contact .message_wrapper:hover .timestamp_action,
-.message_type_call .message_wrapper:hover .timestamp_action {
-    opacity: 1;
-}
-
-/* Ellipsis - dropdown menu */
-
-input[type=checkbox] {
-    display: none;
-}
-
-.menu_interaction {
-    margin: 5px;
-    padding: 10px;
-    padding-top: 0;
-    opacity: 0;
-    height: 20px;
-    transition: visibility 0.3s linear, opacity 0.3s linear;
-}
-
-.message_type_contact .menu_interaction {
-    display: none;
-    visibility: hidden;
-}
-
-.message_type_call .menu_interaction {
-    margin: auto;
-    padding: 0;
-    vertical-align: center;
-}
-
-    .message_type_call .menu_interaction .dropdown {
-        margin-top: -17px;
-    }
-
-.message:hover:not(.message_type_contact) .menu_interaction {
-    display: block;
-    opacity: 1;
-}
-
-.dropdown {
-    display: none;
-    z-index: 1;
-    position: absolute;
-    background-color: #fff;
-    padding-top: 3px;
-    padding-bottom: 3px;
-}
-
-    .dropdown div {
-        color: #111;
-        padding: 10px;
-    }
-
-        .dropdown div:hover {
-            background-color: #ddd;
-        }
-
-.showmenu:checked ~ .dropdown {
-    display: block;
-}
-
-.menuoption {
-    user-select: none;
-    cursor: pointer;
-}
-
-/* Buttons */
-
-.flat-button {
-    border: 0;
-    border-radius: 5px;
-    transition: all 0.3s ease;
-    color: #f9f9f9;
-    padding: 10px 20px 10px 20px;
-    vertical-align: middle;
-    cursor: pointer;
-    flex: 1;
-    padding: 0;
-}
-
-.left_buttons {
-    align-self: center;
-    max-width: 90px;
-    padding-left: 1em;
-}
-
-/* Status */
-
-.status_circle {
-    fill: #A0A0A0;
-    -webkit-animation: circle-dance;
-    -webkit-animation-duration: 0.8s;
-    -webkit-animation-iteration-count: infinite;
-    -webkit-animation-direction: alternate;
-    -webkit-animation-timing-function: ease-in-out;
-}
-
-.anim-first {
-    -webkit-animation-delay: 0.7s;
-}
-
-.anim-second {
-    -webkit-animation-delay: 0.9s;
-}
-
-.anim-third {
-    -webkit-animation-delay: 1.1s;
-}
-
-@-webkit-keyframes circle-dance {
-    0%,50% {
-        -webkit-transform: translateY(0);
-        fill: #A0A0A0;
-    }
-
-    100% {
-        -webkit-transform: translateY(-8px);
-        fill: #000;
-    }
-}
-
-.status-x {
-    stroke-dasharray: 12;
-}
-
-/* Contact + Call interactions */
-.message_type_contact .message_wrapper,
-.message_type_call .message_wrapper {
-    width: auto;
-    margin: auto;
-    display: flex;
-    flex-wrap: wrap;
-    background-color: var(--bg-color);
-    padding: 0;
-}
-
-    .message_type_contact .message_wrapper:before,
-    .message_type_call .message_wrapper:before {
-        display: none;
-    }
-
-.message_type_contact .text,
-.message_type_call .text {
-    align-self: center;
-    font-size: 1.2em;
-    padding: 1em;
-}
-
-/* file interactions */
-
-.message_type_data_transfer .internal_mes_wrapper {
-    padding: 0;
-    display: flex;
-    flex-wrap: wrap;
-}
-
-.invite-btn-red {
-    transition: background-color 0.5s ease;
-}
-
-.invite-btn-red:hover {
-    background: var(--jami-red);
-}
-
-.invite-btn-red svg {
-    fill: var(--jami-red);
-    transition: fill 0.5s ease;
-}
-
-.invite-btn-red:hover svg {
-    fill: var(--invite-hover-color);
-}
-
-.invite-btn-green {
-    transition: background-color 0.5s ease;
-}
-
-.invite-btn-green:hover {
-    background: var(--jami-green);
-}
-
-.invite-btn-green svg {
-    fill: var(--jami-green);
-    transition: fill 0.5s ease;
-}
-
-.invite-btn-green:hover svg {
-    fill: var(--invite-hover-color);
-}
-
-.accept, .refuse {
-    border-radius: 50%;
-    cursor: pointer;
-}
-
-.accept svg,
-.refuse svg {
-    padding: 8px;
-    width: 24px;
-    height: 24px;
-}
-
-.accept {
-    fill: var(--jami-green);
-}
-
-.accept:hover {
-    fill: var(--invite-hover-color);
-    background: var(--jami-green-hover);
-}
-
-.refuse {
-    fill: var(--jami-red);
-}
-
-.refuse:hover {
-    fill: var(--invite-hover-color);
-    background: var(--jami-red-hover);
-}
-
-.message_type_data_transfer .text {
-    text-align: left;
-    align-self: left;
-    padding: 1em;
-}
-
-.truncate-ellipsis {
-    display: table;
-    table-layout: fixed;
-    width: 100%;
-    white-space: nowrap;
-}
-
-    .truncate-ellipsis > * {
-        display: table-cell;
-        overflow: hidden;
-        text-overflow: ellipsis;
-    }
-
-.message_type_data_transfer .filename {
-    cursor: pointer;
-    font-size: 1.1em;
-}
-
-.message_type_data_transfer .informations {
-    color: #555;
-    font-size: 0.8em;
-}
-
-.message_progress_bar {
-    width: 100%;
-    height: 1em;
-    position: relative;
-    overflow: hidden;
-    background-color: #eee;
-    border-radius: 0;
-    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
-}
-
-    .message_progress_bar > span {
-        display: inline;
-        height: 100%;
-        background-color: #01a2b8;
-        position: absolute;
-        overflow: hidden;
-    }
-
-/* text interactions */
-
-.message_type_text .internal_mes_wrapper {
-    padding: 0px;
-}
-
-.message_text {
-    word-break: break-all;
-    word-wrap: hyphenate;
-    max-width: 100%;
-}
-
-    .message_text pre {
-        display: inline;
-    }
-
-pre {
-    font: inherit;
-    font-family: inherit;
-    font-size: inherit;
-    font-style: inherit;
-    font-variant: inherit;
-    font-weight: inherit;
-    margin: 0;
-    padding: 0;
-    white-space: pre-wrap;
-}
-
-/* Media interactions */
-.media_wrapper img {
-    max-width: 800px;
-    max-height: 700px;
-    margin: 2px 0 2px 0;
-    border-radius: 10px;
-}
-
-.playVideo {
-    background-color: rgba(0, 0, 0, 0.6);
-    height: 50px;
-    width: 50px;
-    border-radius: 5px;
-    float: right;
-    position: absolute;
-    top: calc(50% - 25px);
-    left: calc(50% - 25px);
-    z-index: 3;
-}
-
-.containerVideo {
-    width: 100%;
-    position: relative;
-}
-
-.playVideo svg {
-    height: 40px;
-    width: 40px;
-    margin: 5px;
-}
-
-/* Text interaction */
-.failure,
-.sending {
-    margin: 10px 10px;
-    color: #A0A0A0;
-}
-
-/* classic screens */
-@media screen and (max-width: 1920px), screen and (max-height: 1080px) {
-    .message_in {
-        padding-left: 15%;
-    }
-
-    .message_out {
-        padding-right: 15%;
-    }
-
-    .internal_mes_wrapper {
-        max-width: 60%;
-    }
-
-    .media_wrapper img {
-        /* It is perfectly fine to specify max-widths in px when the
-         wrapper's max-width is in %, as long as the max-width in px
-         doesn't exceed the one in %. */
-        max-width: 450px;
-        max-height: 450px;
-    }
-
-    .menu_interaction {
-        margin: 5px;
-        padding: 2px;
-        height: 10px;
-        font-size: 0.7em;
-        transition: visibility 0.3s linear,opacity 0.3s linear;
-    }
-}
-
-/* lower resolutions */
-@media screen and (max-width: 1000px), screen and (max-height: 480px) {
-    .message_in {
-        padding-left: 0;
-    }
-
-    .message_out {
-        padding-right: 0;
-    }
-
-    .message_type_contact,
-    .message_type_call {
-        max-width: 100%;
-    }
-
-    .internal_mes_wrapper {
-        max-width: 90%;
-    }
-
-    /* Media interactions */
-    .media_wrapper img {
-        max-width: 300px;
-        max-height: 300px;
-    }
-}
-
-@media screen and (max-width: 550px) {
-    .message_type_data_transfer .message_wrapper {
-        width: 250px;
-    }
-}
-
-/* Special case */
-@media screen and (max-width: 350px) {
-    .sender_image {
-        display: none;
-    }
-
-    /* File interactions */
-    .message_type_data_transfer .left_buttons {
-        max-width: 100%;
-    }
-
-    .message_type_data_transfer .text {
-        max-width: 100%;
-        padding-left: 0;
-    }
-
-    .message_type_data_transfer .message_wrapper {
-        width: 200px;
-    }
-}
diff --git a/web/chatview.html b/web/chatview.html
deleted file mode 100644
index 5263398..0000000
--- a/web/chatview.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<html>
-<!-- Empty head might be needed for setSenderImage -->
-<head>
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <meta charset="utf-8">
-    <!--<link rel="stylesheet" href="chatview.css">
-    <script type="text/javascript" src="linkify.js"></script>
-    <script type="text/javascript" src="linkify-html.js"></script>
-    <script type="text/javascript" src="linkify-string.js"></script>
-    <script type="text/javascript" src="qwebchannel.js"></script>-->
-</head>
-<body>
-    <div class="navbar-wrapper">
-        <div id="invitation">
-            <div id="invite_header">
-                <span id="invite_image"></span>
-                <div id="text"></div>
-            </div>
-            <div id="actions">
-                <div id="accept-btn" class="nav-button action-button invite-btn-green" onclick="acceptInvitation()" title="Accept">
-                    <svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
-                        <path d="M0 0h24v24H0z" fill="none" />
-                        <path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
-                    </svg>
-                </div>
-                <div id="refuse-btn" class="nav-button action-button invite-btn-red" onclick="refuseInvitation()" title="Refuse">
-                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-                        <path fill="none" d="M0 0h24v24H0V0z" />
-                        <path d="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" />
-                    </svg>
-                </div>
-                <div id="block-btn" class="nav-button action-button invite-btn-red" onclick="blockConversation()" title="Block">
-                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-                        <path d="M0 0h24v24H0z" fill="none" />
-                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z" />
-                    </svg>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div id="container">
-        <div id="messages" onscroll="onScrolled()"></div>
-        <div id="back_to_bottom_button_container">
-            <div id="back_to_bottom_button" onclick="back_to_bottom()">Jump to latest &#9660;</div>
-        </div>
-        <div id="file_image_send_container"></div>
-        <div id="sendMessage">
-            <div class="nav-button action-button" onclick="selectFile()" title="Send File">
-                <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-                    <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
-                    <path d="M0 0h24v24H0z" fill="none" />
-                </svg>
-            </div>
-            <textarea id="message" autofocus placeholder="Type a message" onkeyup="grow_text_area()" onkeydown="process_messagebar_keydown()" rows="1"></textarea>
-            <div class="nav-button action-button" onclick="sendMessage(); grow_text_area()" title="Send">
-                <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-                    <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
-                    <path d="M0 0h24v24H0z" fill="none" />
-                </svg>
-            </div>
-        </div>
-    </div>
-</body>
-</html>
diff --git a/web/chatview.js b/web/chatview.js
deleted file mode 100644
index a1515f6..0000000
--- a/web/chatview.js
+++ /dev/null
@@ -1,1794 +0,0 @@
-"use strict"
-
-/* Constants used at several places*/
-const messageBarPlaceHolder = "Type a message"
-/* Constants used at several places*/
-// scrollDetectionThresh represents the number of pixels a user can scroll
-// without disabling the automatic go-back-to-bottom when a new message is
-// received
-const scrollDetectionThresh = 200
-// printHistoryPart loads blocks of messages. Each block contains
-// scrollBuffer messages
-const scrollBuffer = 20
-// The first time a conversation is loaded, the lazy loading system makes
-// sure at least initialScrollBufferFactor screens of messages are loaded
-const initialScrollBufferFactor = 3
-// Some signal like the onscrolled signals are debounced so that the their
-// assigned function isn't fired too often
-const debounceTime = 200
-
-/* Buffers */
-// current index in the history buffer
-var historyBufferIndex = 0
-// buffer containing the conversation's messages
-var historyBuffer = []
-
-/* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */
-/* NOTE: always use getElementById when possible, way more efficient */
-const messageBar        = document.getElementById("sendMessage")
-const messageBarInput   = document.getElementById("message")
-const invitation        = document.getElementById("invitation")
-const inviteImage       = document.getElementById("invite_image")
-const invitationText    = document.getElementById("text")
-var   messages          = document.getElementById("messages")
-var   backToBottomBtn   = document.getElementById("back_to_bottom_button")
-var   sendContainer     = document.getElementById("file_image_send_container")
-
-messageBarInput.onpaste = pasteKeyDetected;
-
-/* States: allows us to avoid re-doing something if it isn't meaningful */
-var displayLinksEnabled = true
-var hoverBackButtonAllowed = true
-var hasInvitation = false
-var isTemporary = false
-var isBanned = false
-var isAccountEnabled = true
-var isInitialLoading = false
-var imagesLoadingCounter = 0
-var canLazyLoad = false;
-
-/* Set the default target to _self and handle with QWebEnginePage::acceptNavigationRequest */
-var linkifyOptions = {
-    attributes: null,
-    className: 'linkified',
-    defaultProtocol: 'http',
-    events: null,
-    format: function (value, type) {
-        return value;
-    },
-    formatHref: function (href, type) {
-        return href;
-    },
-    ignoreTags: [],
-    nl2br: false,
-    tagName: 'a',
-    target: {
-        url: '_self'
-    },
-    validate: true
-};
-
-new QWebChannel(qt.webChannelTransport, function(channel) {
-    window.jsbridge = channel.objects.jsbridge;
-});
-
-function onScrolled_() {
-    if (!canLazyLoad)
-        return;
-    // back to bottom button
-    if(messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) {
-        // fade out
-        if (!backToBottomBtn.classList.contains('fade')) {
-            backToBottomBtn.classList.add('fade');
-            backToBottomBtn.removeAttribute("onclick");
-        }
-    } else {
-        // fade in
-        if (backToBottomBtn.classList.contains('fade')) {
-            backToBottomBtn.style.visibility = "visible";
-            backToBottomBtn.classList.remove('fade');
-            backToBottomBtn.onclick = back_to_bottom;
-        }
-    }
-    if (messages.scrollTop == 0 && historyBufferIndex != historyBuffer.length) {
-        /* At the top and there's something to print */
-        printHistoryPart(messages, messages.scrollHeight)
-    }
-}
-
-const debounce = (fn, time) => {
-    let timeout
-
-    return function() {
-        const functionCall = () => fn.apply(this, arguments)
-
-        clearTimeout(timeout)
-        timeout = setTimeout(functionCall, time)
-    }
-}
-
-/* exported onScrolled */
-var onScrolled = debounce(onScrolled_, debounceTime)
-
-/**
- * Generic wrapper. Execute passed function keeping scroll position identical.
- *
- * @param func function to execute
- * @param args parameters as array
- */
-function exec_keeping_scroll_position(func, args) {
-    var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh
-    func(...args)
-    if (atEnd) {
-        messages.scrollTop = messages.scrollHeight
-    }
-}
-
-/**
- * Reset scrollbar at a given position.
- * @param scroll position at which the scrollbar should be set.
- *               Here position means the number of pixels scrolled,
- *               i.e. scroll = 0 resets the scrollbar at the bottom.
- */
-function back_to_scroll(scroll) {
-    messages.scrollTop = messages.scrollHeight - scroll
-}
-
-/**
- * Reset scrollbar at bottom.
- */
-function back_to_bottom() {
-    back_to_scroll(0)
-}
-
-/**
- * Hide or show invitation.
- *
- * Invitation is hidden if no contactAlias/invalid alias is passed.
- * Otherwise, invitation div is updated.
- *
- * @param contactAlias
- * @param contactId
- */
-/* exported showInvitation */
-function showInvitation(contactAlias, contactId) {
-    if (!contactAlias) {
-        if (hasInvitation) {
-            hasInvitation = false
-            invitation.style.visibility = ""
-        }
-    } else {
-        if (!inviteImage.classList.contains('invite_sender_image')) {
-            inviteImage.classList.add('invite_sender_image');
-        }
-        const className = `invite_sender_image_${contactId}`.replace(/@/g, "_").replace(/\./g, "_")
-        if (!inviteImage.classList.contains(className)) {
-            inviteImage.classList.add(className);
-        }
-        hasInvitation = true
-        invitationText.innerHTML = "<span id='invite_contact_name'>" + contactAlias + "</span> is not in your contacts<br/>"
-            + "Note: you can automatically accept this invitation by sending a message."
-        invitation.style.visibility = "visible"
-    }
-}
-
-/* exported setDisplayLinks */
-function setDisplayLinks(display) {
-    displayLinksEnabled = display
-}
-
-/**
- * This event handler dynamically resizes the message bar depending on the amount of
- * text entered, while adjusting the body paddings so that that the message bar doesn't
- * overlap messages when it grows.
- */
-/* exported grow_text_area */
-function grow_text_area() {
-    exec_keeping_scroll_position(function(){
-        var old_height = window.getComputedStyle(messageBar).height
-        messageBarInput.style.height = "auto" /* <-- necessary, no clue why */
-        messageBarInput.style.height = messageBarInput.scrollHeight + "px"
-        var new_height = window.getComputedStyle(messageBar).height
-
-        var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size")
-        var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height)
-
-        document.body.style.setProperty("--messagebar-size", total_size.toString() + "px")
-    }, [])
-}
-
-/**
- * This event handler processes keydown events from the message bar. When pressed key is
- * the enter key, send the message unless shift or control was pressed too.
- *
- * @param key the pressed key
- */
-/* exported process_messagebar_keydown */
-function process_messagebar_keydown(key) {
-    key = key || event
-    var map = {}
-    map[key.keyCode] = key.type == "keydown"
-    if (key.ctrlKey && map[13]) {
-        messageBarInput.value += "\n"
-    }
-    if (key.ctrlKey || key.shiftKey) {
-        return true
-    }
-    if (map[13]) {
-        sendMessage()
-        key.preventDefault()
-    }
-    return true
-}
-
-/**
- * Disable or enable textarea.
- *
- * @param isDisabled whether message bar should be enabled or disabled
- */
-/* exported disableSendMessage */
-function disableSendMessage(isDisabled)
-{
-    messageBarInput.disabled = isDisabled
-}
-
-/*
- * Update timestamps messages.
- */
-function updateView() {
-    updateTimestamps(messages)
-}
-
-setInterval(updateView, 60000)
-
-/**
- * Transform a date to a string group like "1 hour ago".
- *
- * @param date
- */
-function formatDate(date) {
-    const seconds = Math.floor((new Date() - date) / 1000)
-    var interval = Math.floor(seconds / (3600 * 24))
-    if (interval > 5) {
-        return date.toLocaleDateString()
-    }
-    if (interval > 1) {
-        return interval + "\u200E days ago"
-    }
-    if (interval === 1) {
-        return interval + "\u200E day ago"
-    }
-    interval = Math.floor(seconds / 3600)
-    if (interval > 1) {
-        return interval + "\u200E hours ago"
-    }
-    if (interval === 1) {
-        return interval + "\u200E hour ago"
-    }
-    interval = Math.floor(seconds / 60)
-    if (interval > 1) {
-        return interval + "\u200E minutes ago"
-    }
-    return "just now"
-}
-
-/**
- * Send content of message bar
- */
-function sendMessage()
-{
-    //send image in sendContainer
-    var data_to_send = sendContainer.innerHTML;
-    var imgSrcExtract = new RegExp('<img src="(.*?)">', 'g');
-    var fileSrcExtract = new RegExp('<div class="file_wrapper" data-path="(.*?)">', 'g');
-
-    var img_src;
-    while ((img_src = imgSrcExtract.exec(data_to_send)) !== null ) {
-        window.jsbridge.sendImage(img_src[1]);
-    }
-
-    var file_src;
-    while ((file_src = fileSrcExtract.exec(data_to_send)) !== null) {
-        window.jsbridge.sendFile(file_src[1]);
-    }
-
-    sendContainer.innerHTML = "";
-    sendContainer.style.visibility = "hidden";
-    reduce_send_container();
-
-    var message = messageBarInput.value
-    if (message.length > 0) {
-        messageBarInput.value = ""
-        window.jsbridge.sendMessage(message)
-    }
-
-}
-
-/* exported acceptInvitation */
-function acceptInvitation() {
-    window.jsbridge.acceptInvitation()
-}
-/* exported refuseInvitation */
-function refuseInvitation() {
-    window.jsbridge.refuseInvitation()
-}
-/* exported blockConversation */
-function blockConversation() {
-    window.jsbridge.blockConversation()
-}
-
-/**
- * Convert text to HTML.
- */
-function escapeHtml(html)
-{
-    var text = document.createTextNode(html)
-    var div = document.createElement("div")
-    div.appendChild(text)
-    return div.innerHTML
-}
-
-/**
- * Get the youtube video id from a URL.
- * @param url
- */
-function youtube_id(url) {
-    const regExp = /^.*(youtu\.be\/|v\/|\/u\/w|embed\/|watch\?v=|&v=)([^#&?]*).*/
-    const match = url.match(regExp)
-    return (match && match[2].length == 11) ? match[2] : null
-}
-
-/**
- * Returns HTML message from the message text, cleaned and linkified.
- * @param message_text
- */
-function getMessageHtml(message_text)
-{
-    const escaped_message = escapeHtml(message_text)
-
-    var linkified_message = linkifyHtml(escaped_message, linkifyOptions)  // eslint-disable-line no-undef
-
-    const textPart = document.createElement("pre")
-    textPart.innerHTML = linkified_message
-
-    return textPart.outerHTML
-}
-
-/**
- * Returns the message status, formatted for display
- * @param message_delivery_status
- */
-/* exported getMessageDeliveryStatusText */
-function getMessageDeliveryStatusText(message_delivery_status)
-{
-    var formatted_delivery_status = message_delivery_status
-
-    switch(message_delivery_status)
-    {
-    case "sending":
-    case "ongoing":
-        formatted_delivery_status = "Sending<svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><circle class='status_circle anim-first' cx='4' cy='12' r='1'/><circle class='status_circle anim-second' cx='8' cy='12' r='1'/><circle class='status_circle anim-third' cx='12' cy='12' r='1'/></svg>"
-        break
-    case "failure":
-        formatted_delivery_status = "Failure <svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><path class='status-x x-first' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M4,4 L12,12'/><path class='status-x x-second' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M12,4 L4,12'/></svg>"
-        break
-    case "sent":
-    case "finished":
-    case "unknown":
-    case "read":
-        formatted_delivery_status = ""
-        break
-    default:
-        break
-    }
-
-    return formatted_delivery_status
-}
-
-/**
- * Returns the message date, formatted for display
- */
-function getMessageTimestampText(message_timestamp, custom_format)
-{
-    const date = new Date(1000 * message_timestamp)
-    if(custom_format) {
-        return formatDate(date)
-    } else {
-        return date.toLocaleString()
-    }
-}
-
-/**
- * Update timestamps.
- * @param message_div
- */
-function updateTimestamps(messages_div) {
-    const timestamps = messages_div.getElementsByClassName("timestamp")
-    for (var c = timestamps.length - 1; c >= 0 ; --c) {
-        var timestamp = timestamps.item(c)
-        timestamp.innerHTML = getMessageTimestampText(timestamp.getAttribute("message_timestamp"), true)
-    }
-}
-
-/**
- * Convert a value in filesize
- */
-function humanFileSize(bytes) {
-    var thresh = 1024
-    if(Math.abs(bytes) < thresh) {
-        return bytes + " B"
-    }
-    var units = ["kB","MB","GB","TB","PB","EB","ZB","YB"]
-    var u = -1
-    do {
-        bytes /= thresh
-        ++u
-    } while(Math.abs(bytes) >= thresh && u < units.length - 1)
-    return bytes.toFixed(1)+" "+units[u]
-}
-
-/**
- * Hide or show add to conversations/calls whether the account is enabled
- * @param accountEnabled true if account is enabled
- */
-function hideControls(accountEnabled) {
-    if (!accountEnabled) {
-        callButtons.display = "none"
-    } else {
-        callButtons.display = ""
-    }
-}
-
-/**
- * Change the value of the progress bar.
- *
- * @param progress_bar
- * @param message_object
- */
-function updateProgressBar(progress_bar, message_object) {
-    var delivery_status = message_object["delivery_status"]
-    if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) {
-        var progress_percent = (100 * message_object["progress"] / message_object["totalSize"])
-        if (progress_percent !== 100)
-            progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%")
-        else
-            progress_bar.setAttribute("style", "display: none")
-    } else
-        progress_bar.setAttribute("style", "display: none")
-}
-
-/**
- * Check if a status is an error status
- * @param
- */
-function isErrorStatus(status) {
-    return (status === "failure"
-         || status === "awaiting peer timeout"
-         || status === "canceled"
-         || status === "unjoinable peer")
-}
-
-/**
- * Build a new file interaction
- * @param message_id
- */
-function fileInteraction(message_id, message_direction) {
-    var message_wrapper = document.createElement("div")
-    message_wrapper.setAttribute("class", "message_wrapper")
-
-    var transfer_info_wrapper = document.createElement("div")
-    transfer_info_wrapper.setAttribute("class", "transfer_info_wrapper")
-    message_wrapper.appendChild(transfer_info_wrapper)
-
-    /* Buttons at the left for status information or accept/refuse actions.
-       The text is bold and clickable. */
-    var left_buttons = document.createElement("div")
-    left_buttons.setAttribute("class", "left_buttons")
-    transfer_info_wrapper.appendChild(left_buttons)
-
-    var full_div = document.createElement("div")
-    full_div.setAttribute("class", "full")
-    full_div.style.visibility = "hidden"
-    full_div.style.display = "none"
-
-    var filename_wrapper = document.createElement("div")
-    filename_wrapper.setAttribute("class", "truncate-ellipsis")
-
-    var message_text = document.createElement("span")
-    message_text.setAttribute("class", "filename")
-    filename_wrapper.appendChild(message_text)
-
-    // And information like size or error message.
-    var informations_div = document.createElement("div")
-    informations_div.setAttribute("class", "informations")
-
-    var text_div = document.createElement("div")
-    text_div.setAttribute("class", "text")
-    text_div.addEventListener("click", function () {
-        // ask ring to open the file
-        const filename = document.querySelector("#message_" + message_id + " .full").innerText
-        window.jsbridge.openFile(filename);
-    })
-
-    text_div.appendChild(filename_wrapper)
-    text_div.appendChild(full_div)
-    text_div.appendChild(informations_div)
-    transfer_info_wrapper.appendChild(text_div)
-
-    // And finally, a progress bar
-    var message_transfer_progress_bar = document.createElement("span")
-    message_transfer_progress_bar.setAttribute("class", "message_progress_bar")
-
-    var message_transfer_progress_completion = document.createElement("span")
-    message_transfer_progress_bar.appendChild(message_transfer_progress_completion)
-    message_wrapper.appendChild(message_transfer_progress_bar)
-
-    const internal_mes_wrapper = document.createElement("div")
-    internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper")
-
-    var tbl = buildMsgTable(message_direction);
-    var msg_cell = tbl.querySelector(".msg_cell");
-    msg_cell.appendChild(message_wrapper);
-
-    internal_mes_wrapper.appendChild(tbl);
-
-    return internal_mes_wrapper
-}
-
-function buildMsgTable(message_direction) {
-    var tbl = document.createElement("table");
-
-    var row0 = document.createElement("tr");
-    var sender_image_cell = document.createElement("td");
-    sender_image_cell.setAttribute("class", "sender_image_cell")
-    var msg_cell = document.createElement("td");
-    msg_cell.setAttribute("class", "msg_cell")
-
-    row0.appendChild((message_direction === "in") ? sender_image_cell : msg_cell);
-    row0.appendChild((message_direction === "in") ? msg_cell : sender_image_cell);
-
-    tbl.appendChild(row0);
-
-    var row1 = document.createElement("tr");
-    var dummy_cell = document.createElement("td");
-    dummy_cell.setAttribute("class", "dummy_cell")
-    var timestamp_cell = document.createElement("td");
-    timestamp_cell.setAttribute("class", "timestamp_cell")
-
-    row1.appendChild((message_direction === "in") ? dummy_cell : timestamp_cell);
-    row1.appendChild((message_direction === "in") ? timestamp_cell : dummy_cell);
-
-    tbl.appendChild(row1);
-
-    return tbl;
-}
-
-/**
- * Build information text for passed file interaction message object
- *
- * @param message_object message object containing file interaction info
- */
-function buildFileInformationText(message_object) {
-    var informations_txt = getMessageTimestampText(message_object["timestamp"], true)
-    if (message_object["totalSize"] && message_object["progress"]) {
-        if (message_object["delivery_status"] === "finished") {
-            informations_txt += " - " + humanFileSize(message_object["totalSize"])
-        } else {
-            informations_txt += " - " + humanFileSize(message_object["progress"])
-                         + " / " + humanFileSize(message_object["totalSize"])
-        }
-    }
-
-    return informations_txt + " - " + message_object["delivery_status"]
-}
-
-/**
- * Update a file interaction (icons + filename + status + progress bar)
- *
- * @param message_div the message to update
- * @param message_object new informations
- * @param forceTypeToFile
- */
-function updateFileInteraction(message_div, message_object, forceTypeToFile = false) {
-    if (!message_div.querySelector(".informations")) return // media
-
-    var acceptSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z\"/></svg>",
-        refuseSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>",
-        fileSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>",
-        warningSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/></svg>"
-    var message_delivery_status = message_object["delivery_status"]
-    var message_direction = message_object["direction"]
-    var message_id = message_object["id"]
-    var message_text = message_object["text"]
-
-    if (isImage(message_text) && message_delivery_status === "finished" && displayLinksEnabled && !forceTypeToFile) {
-        // Replace the old wrapper by the downloaded image
-        var old_wrapper = message_div.querySelector(".internal_mes_wrapper")
-        if (old_wrapper) {
-            old_wrapper.parentNode.removeChild(old_wrapper)
-        }
-
-        var errorHandler = function() {
-            var wrapper = message_div.querySelector(".internal_mes_wrapper")
-            var message_wrapper = message_div.querySelector(".message_wrapper")
-            if (message_wrapper) {
-                message_wrapper.parentNode.removeChild(message_wrapper)
-            }
-
-            var media_wrapper = message_div.querySelector(".media_wrapper")
-            if (media_wrapper) {
-                media_wrapper.parentNode.removeChild(media_wrapper)
-            }
-
-            var new_interaction = fileInteraction(message_id, message_direction)
-            var new_message_wrapper = new_interaction.querySelector(".message_wrapper")
-            wrapper.prepend(new_message_wrapper)
-            updateFileInteraction(message_div, message_object, true)
-        }
-
-        var new_wrapper = mediaInteraction(message_id, message_direction, message_text, null, errorHandler)
-        message_div.insertBefore(new_wrapper, message_div.querySelector(".menu_interaction"))
-        message_div.querySelector("img").id = message_id
-        message_div.querySelector("img").msg_obj = message_object
-        addSenderImage(message_div, message_object["type"], message_object["sender_contact_method"]);
-        return
-    }
-
-    // Set informations text
-    var informations_div = message_div.querySelector(".informations")
-    informations_div.innerText = buildFileInformationText(message_object)
-
-    // Update flat buttons
-    var left_buttons = message_div.querySelector(".left_buttons")
-    left_buttons.innerHTML = ""
-    if (message_delivery_status === "awaiting peer" ||
-        message_delivery_status === "awaiting host" ||
-        message_delivery_status.indexOf("ongoing") === 0) {
-
-        if (message_direction === "in" && message_delivery_status.indexOf("ongoing") !== 0) {
-            // add buttons to accept or refuse a call.
-            var accept_button = document.createElement("div")
-            accept_button.innerHTML = acceptSvg
-            accept_button.setAttribute("title", "Accept")
-            accept_button.setAttribute("class", "flat-button accept")
-            accept_button.onclick = function() {
-                window.jsbridge.acceptFile(message_id);
-            }
-            left_buttons.appendChild(accept_button)
-        }
-
-        var refuse_button = document.createElement("div")
-        refuse_button.innerHTML = refuseSvg
-        refuse_button.setAttribute("title", "Refuse")
-        refuse_button.setAttribute("class", "flat-button refuse")
-        refuse_button.onclick = function() {
-            window.jsbridge.refuseFile(message_id);
-        }
-        left_buttons.appendChild(refuse_button)
-    } else {
-        var status_button = document.createElement("div")
-        var statusFile = fileSvg
-        if (isErrorStatus(message_delivery_status))
-            statusFile = warningSvg
-        status_button.innerHTML = statusFile
-        status_button.setAttribute("class", "flat-button")
-        left_buttons.appendChild(status_button)
-    }
-
-    message_div.querySelector(".full").innerText = message_text
-    message_div.querySelector(".filename").innerText = message_text.split("/").pop()
-    updateProgressBar(message_div.querySelector(".message_progress_bar"), message_object)
-}
-
-/**
- * Return if a file is an image
- * @param file
- */
-function isImage(file) {
-    return file.toLowerCase().match(/\.(jpeg|jpg|gif|png)$/) !== null
-}
-
-/**
- * Return if a file is a youtube video
- * @param file
- */
-function isVideo(file) {
-    const availableProtocols = ["http:", "https:"]
-    const videoHostname = ["youtube.com", "www.youtube.com", "youtu.be"]
-    const urlParser = document.createElement("a")
-    urlParser.href = file
-    return (availableProtocols.includes(urlParser.protocol) && videoHostname.includes(urlParser.hostname))
-}
-
-/**
- * Build a container for passed video thumbnail
- * @param linkElt video thumbnail div
- */
-function buildVideoContainer(linkElt) {
-    const containerElt = document.createElement("div")
-    containerElt.setAttribute("class", "containerVideo")
-    const playDiv = document.createElement("div")
-    playDiv.setAttribute("class", "playVideo")
-    playDiv.innerHTML = "<svg fill=\"#ffffff\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\
-        <path d=\"M8 5v14l11-7z\"/>\
-        <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\
-    </svg>"
-    linkElt.appendChild(playDiv)
-    containerElt.appendChild(linkElt)
-
-    return containerElt
-}
-
-/**
- * Try to show an image or a video link (youtube for now)
- * @param message_id
- * @param link to show
- * @param ytid if it's a youtube video
- * @param errorHandler the new media's onerror field will be set to this function
- */
-function mediaInteraction(message_id, message_direction, link, ytid, errorHandler) {
-    /* TODO promise?
-     Try to display images. */
-    const media_wrapper = document.createElement("div")
-    media_wrapper.setAttribute("class", "media_wrapper")
-    const linkElt = document.createElement("a")
-    linkElt.href = link
-    linkElt.style.textDecoration = "none"
-    linkElt.style.border = "none"
-    const imageElt = document.createElement("img")
-
-    imageElt.src = ytid ? `http://img.youtube.com/vi/${ytid}/0.jpg` : link
-
-    /* Note, here, we don't check the size of the image.
-     in the future, we can check the content-type and content-length with a request
-     and maybe disable svg */
-
-    if (isInitialLoading) {
-        /* During initial load, make sure the scrollbar stays at the bottom.
-           Also, the final scrollHeight is only known after the last image was
-           loaded. We want to display a specific number of messages screens so
-           we have to set up a callback (on_image_load_finished) which will
-           check on that and reschedule a new display batch if not enough
-           messages have been loaded in the DOM. */
-        imagesLoadingCounter++
-        imageElt.onload = function() {
-            back_to_bottom()
-            on_image_load_finished()
-        }
-
-        if (errorHandler) {
-            imageElt.onerror = function() {
-                errorHandler()
-                back_to_bottom()
-                on_image_load_finished()
-            }
-        }
-    } else if (messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) {
-        /* Keep the scrollbar at the bottom. Images are loaded asynchronously and
-           the scrollbar position is changed each time an image is loaded and displayed.
-           In order to make sure the scrollbar stays at the bottom, reset scrollbar
-           position each time an image was loaded. */
-        imageElt.onload = back_to_bottom
-
-        if (errorHandler) {
-            imageElt.onerror = function() {
-                errorHandler()
-                back_to_bottom()
-            }
-        }
-    } else if (errorHandler) {
-        imageElt.onerror = errorHandler
-    }
-
-    linkElt.appendChild(imageElt)
-
-    if (ytid) {
-        media_wrapper.appendChild(buildVideoContainer(linkElt))
-    } else {
-        media_wrapper.appendChild(linkElt)
-    }
-
-    const internal_mes_wrapper = document.createElement("div")
-    internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper")
-
-    var tbl = buildMsgTable(message_direction);
-    var msg_cell = tbl.querySelector(".msg_cell");
-    msg_cell.appendChild(media_wrapper);
-
-    internal_mes_wrapper.appendChild(tbl)
-
-    return internal_mes_wrapper
-}
-
-/**
- * Build a new text interaction
- * @param message_id
- * @param htmlText the DOM to show
- */
-function textInteraction(message_id, message_direction, htmlText) {
-    const message_wrapper = document.createElement("div")
-    message_wrapper.setAttribute("class", "message_wrapper")
-    var message_text = document.createElement("div")
-    message_text.setAttribute("class", "message_text")
-    message_text.innerHTML = htmlText
-    message_wrapper.appendChild(message_text)
-    // TODO STATUS
-
-    const internal_mes_wrapper = document.createElement("div")
-    internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper")
-
-    var tbl = buildMsgTable(message_direction);
-    var msg_cell = tbl.querySelector(".msg_cell");
-    msg_cell.appendChild(message_wrapper);
-
-    internal_mes_wrapper.appendChild(tbl);
-
-    return internal_mes_wrapper
-}
-
-/**
- * Update a text interaction (text)
- * @param message_div the message to update
- * @param delivery_status the status of the message
- */
-function updateTextInteraction(message_div, delivery_status) {
-    if (!message_div.querySelector(".message_text")) return // media
-    var sending = message_div.querySelector(".sending")
-    switch(delivery_status)
-    {
-    case "ongoing":
-    case "sending":
-        if (!sending) {
-            sending = document.createElement("div")
-            sending.setAttribute("class", "sending")
-            sending.innerHTML = "<svg overflow=\"hidden\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><circle class=\"status_circle anim-first\" cx=\"4\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-second\" cx=\"8\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-third\" cx=\"12\" cy=\"12\" r=\"1\"/></svg>"
-            // add sending animation to message;
-            message_div.insertBefore(sending, message_div.querySelector(".menu_interaction"))
-        }
-        message_div.querySelector(".message_text").style.color = "#888"
-        break
-    case "failure":
-        // change text color to red
-        message_div.querySelector(".message_text").color = "#000"
-        var failure_div = message_div.querySelector(".failure")
-        if (!failure_div) {
-            failure_div = document.createElement("div")
-            failure_div.setAttribute("class", "failure")
-            failure_div.innerHTML = "<svg overflow=\"visible\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><path class=\"status-x x-first\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M4,4 L12,12\"/><path class=\"status-x x-second\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M12,4 L4,12\"/></svg>"
-            // add failure animation to message
-            message_div.insertBefore(failure_div, message_div.querySelector(".menu_interaction"))
-        }
-        message_div.querySelector(".message_text").style.color = "#000"
-        if (sending) sending.style.display = "none"
-        break
-    case "sent":
-    case "finished":
-    case "unknown":
-    case "read":
-        // change text color to black
-        message_div.querySelector(".message_text").style.color = "#000"
-        if (sending) sending.style.display = "none"
-        break
-    default:
-        break
-    }
-}
-
-/**
- * Build a new interaction (call or contact)
- */
-function actionInteraction() {
-    var message_wrapper = document.createElement("div")
-    message_wrapper.setAttribute("class", "message_wrapper")
-
-    // A file interaction contains buttons at the left of the interaction
-    // for the status or accept/refuse buttons
-    var left_buttons = document.createElement("div")
-    left_buttons.setAttribute("class", "left_buttons")
-    message_wrapper.appendChild(left_buttons)
-    // Also contains a bold clickable text
-    var text_div = document.createElement("div")
-    text_div.setAttribute("class", "text")
-    message_wrapper.appendChild(text_div)
-    return message_wrapper
-}
-
-/**
- * Update a call interaction (icon + text)
- * @param message_div the message to update
- * @param message_object new informations
- */
-function updateCallInteraction(message_div, message_object) {
-    const outgoingCall = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z\"/></svg>"
-    const callMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M19.59 7L12 14.59 6.41 9H11V7H3v8h2v-4.59l7 7 9-9z\"/></svg>"
-    const outgoingMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M24 24H0V0h24v24z\" id=\"a\"/></defs><clipPath id=\"b\"><use overflow=\"visible\" xlink:href=\"#a\"/></clipPath><path clip-path=\"url(#b)\" d=\"M3 8.41l9 9 7-7V15h2V7h-8v2h4.59L12 14.59 4.41 7 3 8.41z\"/></svg>"
-    const callReceived = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M20 5.41L18.59 4 7 15.59V9H5v10h10v-2H8.41z\"/></svg>"
-
-    const message_text = message_object["text"]
-    const message_direction = (message_text.toLowerCase().indexOf("incoming") !== -1) ? "in" : "out"
-    const missed = message_text.indexOf("Missed") !== -1
-
-    message_div.querySelector(".text").innerText = message_text.substring(2)
-
-    var left_buttons = message_div.querySelector(".left_buttons")
-    left_buttons.innerHTML = ""
-    var status_button = document.createElement("div")
-    var statusFile = ""
-    if (missed)
-        statusFile = (message_direction === "in") ? callMissed : outgoingMissed
-    else
-        statusFile = (message_direction === "in") ? callReceived : outgoingCall
-    status_button.innerHTML = statusFile
-    status_button.setAttribute("class", "flat-button")
-    left_buttons.appendChild(status_button)
-}
-
-/**
- * Update a contact interaction (icon + text)
- * @param message_div the message to update
- * @param message_object new informations
- */
-function updateContactInteraction(message_div, message_object) {
-    const message_text = message_object["text"]
-
-    message_div.querySelector(".text").innerText = message_text
-
-    var left_buttons = message_div.querySelector(".left_buttons")
-    left_buttons.innerHTML = ""
-    var status_button = document.createElement("div")
-    status_button.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\
-<path d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\"/>\
-<path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>"
-    status_button.setAttribute("class", "flat-button")
-    left_buttons.appendChild(status_button)
-}
-
-/**
- * Remove an interaction from the conversation
- * @param interaction_id
- */
-/* exported removeInteraction */
-function removeInteraction(interaction_id) {
-    var interaction = document.getElementById(`message_${interaction_id}`)
-    if (!interaction) {
-        return
-    }
-
-    if (interaction.previousSibling) {
-        /* if element was the most recently received message, make sure the
-           last_message property is given away to the previous sibling */
-        if (interaction.classList.contains("last_message")) {
-            interaction.previousSibling.classList.add("last_message")
-        }
-
-        /* same for timestamp */
-        var timestamp = interaction.querySelector(".timestamp")
-        var previousTimeStamp = interaction.previousSibling.querySelector(".timestamp")
-        if (timestamp && !previousTimeStamp) {
-            interaction.previousSibling.querySelector(".timestamp_cell").appendChild(timestamp)
-        }
-    }
-
-    var firstMessage = getPreviousInteraction(interaction)
-    var secondMessage = getNextInteraction(interaction)
-
-    updateSequencing(firstMessage, secondMessage)
-
-    interaction.parentNode.removeChild(interaction)
-}
-
-/**
- * Build message dropdown
- * @return a message dropdown for passed message id
- */
-function buildMessageDropdown(message_id) {
-    const menu_element = document.createElement("div")
-    menu_element.setAttribute("class", "menu_interaction")
-    menu_element.innerHTML =
-    `<input type="checkbox" id="showmenu${message_id}" class="showmenu">
-     <label for="showmenu${message_id}">
-       <svg fill="#888888" height="12" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg">
-         <path d="M0 0h24v24H0z" fill="none"/>
-         <path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
-       </svg>
-     </label>`
-    menu_element.onclick = function() {
-        const button = this.querySelector(".showmenu")
-        button.checked = !button.checked
-    }
-    menu_element.onmouseleave = function() {
-        const button = this.querySelector(".showmenu")
-        button.checked = false
-    }
-    const dropdown = document.createElement("div")
-    const dropdown_classes = [
-        "dropdown",
-        `dropdown_${message_id}`
-    ]
-    dropdown.setAttribute("class", dropdown_classes.join(" "))
-
-    const remove = document.createElement("div")
-    remove.setAttribute("class", "menuoption")
-    remove.innerHTML = "Delete"
-    remove.msg_id = message_id
-    remove.onclick = function() {
-        window.jsbridge.deleteInteraction(`${this.msg_id}`);
-    }
-    dropdown.appendChild(remove)
-    menu_element.appendChild(dropdown)
-
-    return menu_element
-}
-
-/**
- * Build a message div for passed message object
- * @param message_object to treat
- */
-function buildNewMessage(message_object) {
-    const message_id = message_object["id"]
-    const message_type = message_object["type"]
-    const message_text = message_object["text"]
-    const message_direction = message_object["direction"]
-    const delivery_status = message_object["delivery_status"]
-    const message_sender_contact_method = message_object["sender_contact_method"]
-
-    var classes = [
-        "message",
-        `message_${message_direction}`,
-        `message_type_${message_type}`
-    ]
-
-    var type = ""
-    var message_div = document.createElement("div")
-    message_div.setAttribute("id", `message_${message_id}`)
-    message_div.setAttribute("class", classes.join(" "))
-
-    // Build message for each types.
-
-    // Build main content
-    if (message_type === "data_transfer") {
-        if (isImage(message_text) && delivery_status === "finished" && displayLinksEnabled) {
-            var errorHandler = function() {
-                var wrapper = message_div.querySelector(".internal_mes_wrapper")
-                var message_wrapper = message_div.querySelector(".message_wrapper")
-                if (message_wrapper) {
-                    message_wrapper.parentNode.removeChild(message_wrapper)
-                }
-
-                var media_wrapper = message_div.querySelector(".media_wrapper")
-                if (media_wrapper) {
-                    media_wrapper.parentNode.removeChild(media_wrapper)
-                }
-
-                var new_interaction = fileInteraction(message_id, message_direction)
-                var new_message_wrapper = new_interaction.querySelector(".message_wrapper")
-                wrapper.prepend(new_message_wrapper)
-                updateFileInteraction(message_div, message_object, true)
-            }
-            message_div.append(mediaInteraction(message_id, message_direction, message_text, null, errorHandler))
-            message_div.querySelector("img").id = message_id
-            message_div.querySelector("img").msg_obj = message_object
-        } else {
-            message_div.append(fileInteraction(message_id, message_direction))
-        }
-    } else if (message_type === "text") {
-        // TODO add the possibility to update messages (remove and rebuild)
-        const htmlText = getMessageHtml(message_text)
-        if (displayLinksEnabled) {
-            const parser = new DOMParser()
-            const DOMMsg = parser.parseFromString(htmlText, "text/xml")
-            const links = DOMMsg.querySelectorAll("a")
-            if (DOMMsg.childNodes.length && links.length) {
-                var isTextToShow = (DOMMsg.childNodes[0].childNodes.length > 1)
-                const ytid = (isVideo(message_text))? youtube_id(message_text) : ""
-                if (!isTextToShow && (ytid || isImage(message_text))) {
-                    type = "media"
-                    message_div.append(mediaInteraction(message_id, message_direction, message_text, ytid))
-                }
-            }
-        }
-        if (type !== "media") {
-            type = "text"
-            message_div.append(textInteraction(message_id, message_direction, htmlText))
-        }
-    } else if (message_type === "call" || message_type === "contact") {
-        message_div.append(actionInteraction())
-    } else {
-        const temp = document.createElement("div")
-        temp.innerText = message_type
-        message_div.appendChild(temp)
-    }
-
-    var message_dropdown = buildMessageDropdown(message_id)
-    if (message_type !== "call") {
-        message_div.appendChild(message_dropdown)
-    } else {
-        var wrapper = message_div.querySelector(".message_wrapper")
-        wrapper.insertBefore(message_dropdown, wrapper.firstChild)
-    }
-
-    // Add sender images if necessary (like if the interaction doesn't take the whole width)
-    addSenderImage(message_div, message_type, message_sender_contact_method)
-
-    return message_div
-}
-
-function addSenderImage(message_div, message_type, message_sender_contact_method) {
-    const need_sender = (message_type === "data_transfer" || message_type === "text")
-    if (need_sender) {
-        var sender_image_cell = message_div.querySelector(".sender_image_cell")
-        if (sender_image_cell) {
-            var message_sender_image = document.createElement("span")
-            var cssSafeStr = message_sender_contact_method.replace(/@/g, "_").replace(/\./g, "_");
-            message_sender_image.setAttribute("class", `sender_image sender_image_${cssSafeStr}`)
-            sender_image_cell.appendChild(message_sender_image)
-        } else {
-            console.warn("can't find sender_image_cell");
-        }
-    }
-}
-
-/**
- * Build a timestamp for passed message object
- * @param message_object to treat
- */
-function buildNewTimestamp(message_object) {
-    const message_type = message_object["type"]
-    const message_direction = message_object["direction"]
-    const message_timestamp = message_object["timestamp"]
-
-    const formattedTimestamp = getMessageTimestampText(message_timestamp, true)
-    const date_elt = document.createElement("div")
-
-    date_elt.innerText = formattedTimestamp
-    var typeIsCallOrContact = (message_type === "call" || message_type === "contact")
-    var timestamp_div_classes = ["timestamp", typeIsCallOrContact ? "timestamp_action" : `timestamp_${message_direction}`]
-    date_elt.setAttribute("class", timestamp_div_classes.join(" "))
-    date_elt.setAttribute("message_timestamp", message_timestamp)
-
-    return date_elt
-}
-
-/**
- * Add a message to the conversation.
- * @param message_object to treat
- * @param new_message if it's a new message or if we need to update
- * @param insert_after if we want the message at the end or the top of the conversation
- * @param messages_div
- */
-function addOrUpdateMessage(message_object, new_message, insert_after = true, messages_div) {
-    const message_id = message_object["id"]
-    const message_type = message_object["type"]
-    const message_direction = message_object["direction"]
-    const delivery_status = message_object["delivery_status"]
-
-    var message_div = messages_div.querySelector("#message_" + message_id)
-    if (new_message) {
-        message_div = buildNewMessage(message_object)
-
-        /* Show timestamp if either:
-           - message has type call or contact
-           - or most recently added timestamp in this set is different
-           - or message is the first message in this set */
-
-        var date_elt = buildNewTimestamp(message_object)
-        var timestamp = messages_div.querySelector(".timestamp")
-
-        if (message_type === "call" || message_type === "contact") {
-            message_div.querySelector(".message_wrapper").appendChild(date_elt)
-        } else if (insert_after || !timestamp || timestamp.className !== date_elt.className
-                                || timestamp.innerHTML !== date_elt.innerHTML) {
-            message_div.querySelector(".timestamp_cell").appendChild(date_elt)
-            if (message_direction === "out")
-                message_div.querySelector(".timestamp_cell").setAttribute("class", "timestamp_cell_out")
-        }
-
-        var isGenerated = message_type === "call" || message_type === "contact"
-        if (isGenerated) {
-            message_div.classList.add("generated_message")
-        }
-
-        if (insert_after) {
-            var previousMessage = messages_div.lastChild
-            messages_div.appendChild(message_div)
-            computeSequencing(previousMessage, message_div, null, insert_after)
-            if (previousMessage) {
-                previousMessage.classList.remove("last_message")
-            }
-            message_div.classList.add("last_message")
-
-            /* When inserting at the bottom we should also check that the
-               previously sent message does not have the same timestamp.
-               If it's the case, remove it.*/
-            var previous_timestamp = message_div.previousSibling.querySelector(".timestamp")
-            if (previous_timestamp &&
-                previous_timestamp.className === date_elt.className &&
-                previous_timestamp.innerHTML === date_elt.innerHTML &&
-                !message_div.previousSibling.classList.contains("last_of_sequence")) {
-                previous_timestamp.parentNode.removeChild(previous_timestamp)
-            }
-        } else {
-            var nextMessage = messages_div.firstChild
-            messages_div.prepend(message_div)
-            computeSequencing(message_div, nextMessage, null, insert_after)
-        }
-    }
-
-    if (isErrorStatus(delivery_status) && message_direction === "out") {
-        const dropdown = messages_div.querySelector(`.dropdown_${message_id}`)
-        if (!dropdown.querySelector(".retry")) {
-            const retry = document.createElement("div")
-            retry.setAttribute("class", "retry")
-            retry.innerHTML = "Retry"
-            retry.msg_id = message_id
-            retry.onclick = function() {
-                window.jsbridge.retryInteraction(`${this.msg_id}`);
-            }
-            dropdown.insertBefore(retry, message_div.querySelector(".delete"))
-        }
-    }
-
-    // Update informations if needed
-    if (message_type === "data_transfer")
-        updateFileInteraction(message_div, message_object)
-    if (message_type === "text" && message_direction === "out")
-    // Modify sent status if necessary
-        updateTextInteraction(message_div, delivery_status)
-    if (message_type === "call")
-        updateCallInteraction(message_div, message_object)
-    if (message_type === "contact")
-        updateContactInteraction(message_div, message_object)
-
-    // Clean timestamps
-    updateTimestamps(messages_div)
-}
-
-function getNextInteraction(interaction, includeLazyLoadedBlock = true)
-{
-    var nextInteraction = interaction.nextSibling
-    if (!nextInteraction && includeLazyLoadedBlock) {
-        var nextBlock = interaction.parentNode.nextElementSibling
-        if (nextBlock) {
-            nextInteraction = nextBlock.firstElementChild
-        }
-    }
-    return nextInteraction
-}
-
-function getPreviousInteraction(interaction, includeLazyLoadedBlock = true)
-{
-    var previousInteraction = interaction.previousSibling
-    if (!previousInteraction && includeLazyLoadedBlock) {
-        var previousBlock = interaction.parentNode.previousElementSibling
-        if (previousBlock) {
-            previousInteraction = previousBlock.lastElementChild
-        }
-    }
-    return previousInteraction
-}
-
-function isSequenceBreak(firstMessage, secondMessage, insert_after = true)
-{
-    if (!firstMessage || !secondMessage) {
-        return false
-    }
-    var first_message_direction = firstMessage.classList.contains("message_out") ? "out" : "in"
-    var second_message_direction = secondMessage.classList.contains("message_out") ? "out" : "in"
-    if (second_message_direction != first_message_direction) {
-        return true
-    }
-    var firstMessageIsGenerated = firstMessage.classList.contains("generated_message")
-    var secondMessageIsGenerated = secondMessage.classList.contains("generated_message")
-    if (firstMessageIsGenerated != secondMessageIsGenerated) {
-        return true
-    }
-    if (insert_after) {
-        const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper")
-        if (internal_message_wrapper) {
-            const firstTimestamp = internal_message_wrapper.querySelector(".timestamp")
-            return !!(firstTimestamp) && firstTimestamp.innerHTML !== "just now"
-        }
-        return false
-    } else {
-        const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper")
-        if (internal_message_wrapper) {
-            return !!(internal_message_wrapper.querySelector(".timestamp"))
-        }
-        return false
-    }
-}
-
-function updateSequencing(firstMessage, secondMessage)
-{
-    if (firstMessage) {
-        if (secondMessage) {
-            var sequence_break = isSequenceBreak(firstMessage, secondMessage, false)
-            if (sequence_break) {
-                if (firstMessage.classList.contains("middle_of_sequence")) {
-                    firstMessage.classList.remove("middle_of_sequence")
-                    firstMessage.classList.add("last_of_sequence")
-                } else if (firstMessage.classList.contains("first_of_sequence")) {
-                    firstMessage.classList.remove("first_of_sequence")
-                    firstMessage.classList.add("single_message")
-                }
-                if (secondMessage.classList.contains("middle_of_sequence")) {
-                    secondMessage.classList.remove("middle_of_sequence")
-                    secondMessage.classList.add("first_of_sequence")
-                } else if (secondMessage.classList.contains("last_of_sequence")) {
-                    secondMessage.classList.remove("last_of_sequence")
-                    secondMessage.classList.add("single_message")
-                }
-            } else {
-                if (firstMessage.classList.contains("last_of_sequence")) {
-                    firstMessage.classList.remove("last_of_sequence")
-                    firstMessage.classList.add("middle_of_sequence")
-                } else if (firstMessage.classList.contains("single_message")) {
-                    firstMessage.classList.remove("single_message")
-                    firstMessage.classList.add("first_of_sequence")
-                }
-                if (secondMessage.classList.contains("first_of_sequence")) {
-                    secondMessage.classList.remove("first_of_sequence")
-                    secondMessage.classList.add("middle_of_sequence")
-                } else if (secondMessage.classList.contains("single_message")) {
-                    secondMessage.classList.remove("single_message")
-                    secondMessage.classList.add("last_of_sequence")
-                }
-            }
-        } else {
-            // this is the last interaction of the conversation
-            if (firstMessage.classList.contains("first_of_sequence")) {
-                firstMessage.classList.remove("first_of_sequence")
-                firstMessage.classList.add("single_message")
-            } else if (firstMessage.classList.contains("middle_of_sequence")) {
-                firstMessage.classList.remove("middle_of_sequence")
-                firstMessage.classList.add("last_of_sequence")
-            }
-        }
-    } else if (secondMessage) {
-        // this is the first interaction of the conversation
-        if (secondMessage.classList.contains("middle_of_sequence")) {
-            secondMessage.classList.remove("middle_of_sequence")
-            secondMessage.classList.add("first_of_sequence")
-        } else if (secondMessage.classList.contains("last_of_sequence")) {
-            secondMessage.classList.remove("last_of_sequence")
-            secondMessage.classList.add("single_message")
-        }
-    }
-}
-
-function computeSequencing(firstMessage, secondMessage, lazyLoadingBlock, insert_after = true)
-{
-    if (insert_after) {
-        if (secondMessage) {
-            var secondMessageIsGenerated = secondMessage.classList.contains("generated_message")
-            if (firstMessage && !secondMessageIsGenerated) {
-                var firstMessageIsGenerated = firstMessage.classList.contains("generated_message")
-                var sequence_break = isSequenceBreak(firstMessage, secondMessage)
-                if (sequence_break) {
-                    secondMessage.classList.add("single_message")
-                } else {
-                    if (firstMessage.classList.contains("single_message")) {
-                        firstMessage.classList.remove("single_message")
-                        firstMessage.classList.add("first_of_sequence")
-                    } else if (firstMessage.classList.contains("last_of_sequence")) {
-                        firstMessage.classList.remove("last_of_sequence")
-                        firstMessage.classList.add("middle_of_sequence")
-                    }
-                    if (firstMessageIsGenerated) {
-                        secondMessage.classList.add("single_message")
-                    } else {
-                        secondMessage.classList.add("last_of_sequence")
-                    }
-                }
-            } else if (!secondMessageIsGenerated) {
-                secondMessage.classList.add("single_message")
-            }
-        }
-    } else if (firstMessage) {
-        var firstMessageIsGenerated = firstMessage.classList.contains("generated_message")
-        if (secondMessage && !firstMessageIsGenerated) {
-            var secondMessageIsGenerated = secondMessage.classList.contains("generated_message")
-            var sequence_break = isSequenceBreak(firstMessage, secondMessage, false)
-            if (sequence_break) {
-                firstMessage.classList.add("single_message")
-            } else {
-                if (secondMessage.classList.contains("single_message")) {
-                    secondMessage.classList.remove("single_message")
-                    secondMessage.classList.add("last_of_sequence")
-                } else if (secondMessage.classList.contains("first_of_sequence")) {
-                    secondMessage.classList.remove("first_of_sequence")
-                    secondMessage.classList.add("middle_of_sequence")
-                }
-                if (secondMessageIsGenerated) {
-                    firstMessage.classList.add("single_message")
-                } else {
-                    firstMessage.classList.add("first_of_sequence")
-                }
-            }
-        } else if (!firstMessageIsGenerated) {
-            firstMessage.classList.add("single_message")
-        }
-    }
-}
-
-/**
- * Wrapper for addOrUpdateMessage.
- *
- * Add or update a message and make sure the scrollbar position
- * is refreshed correctly
- *
- * @param message_object message to be added
- */
-/* exported addMessage */
-function addMessage(message_object)
-{
-    if (!messages.lastChild) {
-        var block_wrapper = document.createElement("div")
-        messages.append(block_wrapper)
-    }
-
-    exec_keeping_scroll_position(addOrUpdateMessage, [message_object, true, undefined, messages.lastChild])
-}
-
-/**
- * Update a message that was previously added with addMessage and
- * make sure the scrollbar position is refreshed correctly
- *
- * @param message_object message to be updated
- */
-/* exported updateMessage */
-function updateMessage(message_object)
-{
-    var message_div = messages.querySelector("#message_" + message_object["id"])
-    exec_keeping_scroll_position(addOrUpdateMessage, [message_object, false, undefined, message_div.parentNode])
-}
-
-/**
- * Called whenever an image has finished loading. Check lazy loading status
- * once all images have finished loading.
- */
-function on_image_load_finished() {
-    imagesLoadingCounter--
-
-    if (!imagesLoadingCounter) {
-        /* This code is executed once all images have been loaded. */
-        check_lazy_loading()
-    }
-}
-
-/**
- * Make sure at least initialScrollBufferFactor screens of messages are
- * available in the DOM.
- */
-function check_lazy_loading() {
-    if (!canLazyLoad)
-        return;
-    if (messages.scrollHeight < initialScrollBufferFactor * messages.clientHeight
-        && historyBufferIndex !== historyBuffer.length) {
-        /* Not enough messages loaded, print a new batch. Enable isInitialLoading
-           as reloading a single batch might not be sufficient to fulfill our
-           criteria (we want to be called back again to check on that) */
-        isInitialLoading = true
-        printHistoryPart(messages, 0)
-        isInitialLoading = false
-    }
-}
-
-/**
- * Display 'scrollBuffer' messages from history in passed div (reverse order).
- *
- * @param messages_div that should be modified
- * @param setMessages if enabled, #messages will be set to the resulting messages
- *                    div after being modified. If #messages already exists it will
- *                    be removed and replaced by the new div.
- * @param fixedAt if setMessages is enabled, maintain scrollbar at the specified
- *                position (otherwise modifying #messages would result in
- *                changing the position of the scrollbar)
- */
-function printHistoryPart(messages_div, fixedAt)
-{
-    if (historyBufferIndex === historyBuffer.length) {
-        return
-    }
-    /* If first element is a spinner, remove it */
-    if (messages_div.firstChild && messages_div.firstChild.id === "lazyloading-icon") {
-        messages_div.removeChild(messages_div.firstChild)
-    }
-
-    /* Elements are appended to a wrapper div. This div has no style
-       properties, it allows us to add all messages at once to the main
-       messages div. */
-    var block_wrapper = document.createElement("div")
-
-    messages_div.prepend(block_wrapper)
-
-    for (var i = 0; i < scrollBuffer && historyBufferIndex < historyBuffer.length; ++historyBufferIndex && ++i) {
-        // TODO on-screen messages should be removed from the buffer
-        addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false, block_wrapper)
-    }
-
-    var lastMessage = block_wrapper.lastChild
-
-    updateSequencing(lastMessage, getNextInteraction(lastMessage))
-
-    var absoluteLastMessage = messages_div.lastChild.lastChild
-    if (absoluteLastMessage) {
-        absoluteLastMessage.classList.add("last_message")
-    }
-
-    /* Add ellipsis (...) at the top if there are still messages to load */
-    if (historyBufferIndex < historyBuffer.length) {
-        var llicon = document.createElement("span")
-        llicon.id = "lazyloading-icon"
-        llicon.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"#888888\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"/></svg>"
-        messages_div.prepend(llicon)
-    }
-
-    if (fixedAt !== undefined) {
-        /* update scrollbar position to take text-message -related
-           scrollHeight changes in account (not necessary to wait
-           for DOM redisplay in this case). Changes due to image
-           messages are handled in their onLoad callbacks. */
-        back_to_scroll(fixedAt)
-        /* schedule a scrollbar position update for changes which
-           are neither handled by the previous call nor by onLoad
-           callbacks. This call is necessary but not sufficient,
-           dropping the previous call would result in visual
-           glitches during initial load. */
-        setTimeout(function() {back_to_scroll(fixedAt)}, 0)
-    }
-
-    if (!imagesLoadingCounter) {
-        setTimeout(check_lazy_loading, 0)
-    }
-}
-
-function hideMessagesDiv()
-{
-    if (!messages.classList.contains('fade')) {
-        messages.classList.add('fade');
-    }
-}
-
-function showMessagesDiv()
-{
-    if (messages.classList.contains('fade')) {
-        messages.classList.remove('fade');
-    }
-}
-
-/**
- * Clear all messages.
- */
-/* exported clearMessages */
-function clearMessages()
-{
-    canLazyLoad = false
-
-    backToBottomBtn.style.visibility="hidden";
-
-    while (messages.firstChild) {
-        messages.removeChild(messages.firstChild)
-    }
-
-    window.jsbridge.emitMessagesCleared()
-}
-
-/**
- * Set history buffer, initialize messages div and display a first batch
- * of messages.
- *
- * Make sure that enough messages are displayed to fill initialScrollBufferFactor
- * screens of messages (if enough messages are present in the conversation)
- *
- * @param messages_array should contain history to be printed
- */
-/* exported printHistory */
-function printHistory(messages_array)
-{
-    historyBuffer = messages_array
-    historyBufferIndex = 0
-
-    isInitialLoading = true
-    printHistoryPart(messages, 0)
-    isInitialLoading = false
-
-    canLazyLoad = true
-
-    window.jsbridge.emitMessagesLoaded()
-}
-
-/**
- * Set the image for a given sender
- * set_sender_image object should contain the following keys:
- * - sender: the name of the sender
- * - sender_image: base64 png encoding of the sender image
- *
- * @param set_sender_image_object sender image object as previously described
- */
-/* exported setSenderImage */
-function setSenderImage(set_sender_image_object)
-{
-    var sender_contact_method = set_sender_image_object["sender_contact_method"].replace(/@/g, "_").replace(/\./g, "_"),
-        sender_image = set_sender_image_object["sender_image"],
-        sender_image_id = "sender_image_" + sender_contact_method,
-        invite_sender_image_id = "invite_sender_image_" + sender_contact_method,
-        currentSenderImage = document.getElementById(sender_image_id), // Remove the currently set sender image
-        style, invite_style
-
-    if (currentSenderImage) {
-        currentSenderImage.parentNode.removeChild(currentSenderImage)
-    }
-
-    // Create a new style element
-    style = document.createElement("style")
-
-    style.type = "text/css"
-    style.id = sender_image_id
-    style.innerHTML = "." + sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 2.25em;width: 2.25em;}"
-    document.head.appendChild(style)
-
-    invite_style = document.createElement("style")
-
-    invite_style.type = "text/css"
-    invite_style.id = invite_sender_image_id
-    invite_style.innerHTML = "." + invite_sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 48px;width: 48px;}"
-    document.head.appendChild(invite_style)
-}
-
-/**
- * Copy Mouse Selected Text and return it
- */
-function copy_text_selected() {
-    var selObj = document.getSelection();
-    var selectedText = selObj.toString();
-    return selectedText;
-}
-
-/**
- * Check if text is selected by mouse
- */
-function isTextSelected() {
-
-    var selObj = document.getSelection();
-    var selectedText = selObj.toString();
-
-    if (selectedText.length != 0)
-        return true;
-    return false;
-}
-
-/* exported selectFile - sselect files from Qt */
-function selectFile() {
-
-    window.jsbridge.selectFile();
-    //js or qt
-}
-
-/**
- * add file (local file) to message area
- */
-function addFile_path(path, name, size) {
-    var html = '<div class="file_wrapper" data-path="' + path + '">' +
-        '<svg class="svg-icon" viewBox="0 0 20 20">' +
-        '<path fill = "none" d = "M17.222,5.041l-4.443-4.414c-0.152-0.151-0.356-0.235-0.571-0.235h-8.86c-0.444,0-0.807,0.361-0.807,0.808v17.602c0,0.448,0.363,0.808,0.807,0.808h13.303c0.448,0,0.808-0.36,0.808-0.808V5.615C17.459,5.399,17.373,5.192,17.222,5.041zM15.843,17.993H4.157V2.007h7.72l3.966,3.942V17.993z" ></path>' +
-        '<path fill="none" d="M5.112,7.3c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808c0-0.447-0.363-0.808-0.808-0.808H5.92C5.475,6.492,5.112,6.853,5.112,7.3z"></path>' +
-        '<path fill="none" d="M5.92,5.331h4.342c0.445,0,0.808-0.361,0.808-0.808c0-0.446-0.363-0.808-0.808-0.808H5.92c-0.444,0-0.808,0.361-0.808,0.808C5.112,4.97,5.475,5.331,5.92,5.331z"></path>' +
-        '<path fill="none" d="M13.997,9.218H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,9.58,14.442,9.218,13.997,9.218z"></path>' +
-        '<path fill="none" d="M13.997,11.944H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,12.306,14.442,11.944,13.997,11.944z"></path>' +
-        '<path fill="none" d="M13.997,14.67H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.447,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,15.032,14.442,14.67,13.997,14.67z"></path>' +
-        '</svg >' +
-        '<div class="fileinfo">' +
-        '<p>' + name + '</p>' +
-        '<p>' + size + '</p>' +
-        '</div >' +
-        '<button class="btn" onclick="remove(this)">X</button>' +
-        '</div >';
-    // At first, visiblity can empty
-    if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") {
-        grow_send_container();
-        sendContainer.style.visibility = "visible";
-    }
-    //add html here since display is set to flex, image will change accordingly
-    sendContainer.innerHTML += html;
-}
-
-/**
- * add image (base64 array) to message area
- */
-function addImage_base64(base64) {
-
-    var html =  '<div class="img_wrapper">' +
-                '<img src="data:image/png;base64,' + base64 + '"/>' +
-                '<button class="btn" onclick="remove(this)">X</button>' +
-                '</div >';
-    // At first, visiblity can empty
-    if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") {
-        grow_send_container();
-        sendContainer.style.visibility = "visible";
-    }
-    //add html here since display is set to flex, image will change accordingly
-    sendContainer.innerHTML += html;
-}
-
-/**
- * add image (image path) to message area
- */
-function addImage_path(path) {
-
-    var html = '<div class="img_wrapper">' +
-               '<img src="' + path + '"/>' +
-               '<button class="btn" onclick="remove(this)">X</button>' +
-               '</div >';
-    // At first, visiblity can empty
-    if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") {
-        grow_send_container();
-        sendContainer.style.visibility = "visible";
-    }
-    //add html here since display is set to flex, image will change accordingly
-    sendContainer.innerHTML += html;
-}
-
-/**
- * This function adjusts the body paddings so that that the file_image_send_container doesn't
- * overlap messages when it grows.
- */
-/* exported grow_send_container */
-function grow_send_container() {
-    exec_keeping_scroll_position(function () {
-        var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size");
-        document.body.style.paddingBottom = (parseInt(msgbar_size) + 158).toString() + "px";
-        //6em
-    }, [])
-}
-
-/**
- * This function adjusts the body paddings so that that the file_image_send_container will hide
- * and recover padding bottom
- */
-/* exported grow_send_container */
-function reduce_send_container() {
-    exec_keeping_scroll_position(function () {
-        document.body.style.paddingBottom = (parseInt(document.body.style.paddingBottom) - 158).toString() + "px";
-        //6em
-    }, [])
-}
-
-// Remove current cancel button division  and hide the sendContainer
-function remove(e) {
-    e.parentNode.parentNode.removeChild(e.parentNode);
-    if (sendContainer.innerHTML.length == 0) {
-        reduce_send_container();
-        sendContainer.style.visibility = "hidden";
-    }
-}
-
-function pasteKeyDetected(e) {
-    e.preventDefault();
-    window.jsbridge.emitPasteKeyDetected();
-}
-
-// Set the curser to a target position
-function setCaretPosition(elem, caretPos) {
-    var range;
-
-    if (elem.createTextRange) {
-        range = elem.createTextRange();
-        range.move('character', caretPos);
-        range.select();
-    } else {
-        elem.focus();
-        if (elem.selectionStart !== undefined) {
-            elem.setSelectionRange(caretPos, caretPos);
-        }
-    }
-}
-function replaceText(text) {
-    var input = messageBarInput;
-    var currentContent = input.value;
-    var start = input.selectionStart;
-    var end = input.selectionEnd;
-    var output = [currentContent.slice(0, start), text, currentContent.slice(end)].join('');
-    input.value = output;
-    setCaretPosition(input, start + text.length);
-}
diff --git a/web/linkify-html.js b/web/linkify-html.js
deleted file mode 100644
index 1e5090c..0000000
--- a/web/linkify-html.js
+++ /dev/null
@@ -1,827 +0,0 @@
-/*
- * Copyright (c) 2016 SoapBox Innovations Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
-*/
-
-'use strict';
-
-;(function (window, linkify) {
-  var linkifyHtml = function (linkify) {
-    'use strict';
-
-    var HTML5NamedCharRefs = {};
-
-    function EntityParser(named) {
-      this.named = named;
-    }
-
-    var HEXCHARCODE = /^#[xX]([A-Fa-f0-9]+)$/;
-    var CHARCODE = /^#([0-9]+)$/;
-    var NAMED = /^([A-Za-z0-9]+)$/;
-
-    EntityParser.prototype.parse = function (entity) {
-      if (!entity) {
-        return;
-      }
-      var matches = entity.match(HEXCHARCODE);
-      if (matches) {
-        return '&#x' + matches[1] + ';';
-      }
-      matches = entity.match(CHARCODE);
-      if (matches) {
-        return '&#' + matches[1] + ';';
-      }
-      matches = entity.match(NAMED);
-      if (matches) {
-        return '&' + matches[1] + ';';
-      }
-    };
-
-    var WSP = /[\t\n\f ]/;
-    var ALPHA = /[A-Za-z]/;
-    var CRLF = /\r\n?/g;
-
-    function isSpace(char) {
-      return WSP.test(char);
-    }
-
-    function isAlpha(char) {
-      return ALPHA.test(char);
-    }
-
-    function preprocessInput(input) {
-      return input.replace(CRLF, "\n");
-    }
-
-    function EventedTokenizer(delegate, entityParser) {
-      this.delegate = delegate;
-      this.entityParser = entityParser;
-
-      this.state = null;
-      this.input = null;
-
-      this.index = -1;
-      this.line = -1;
-      this.column = -1;
-      this.tagLine = -1;
-      this.tagColumn = -1;
-
-      this.reset();
-    }
-
-    EventedTokenizer.prototype = {
-      reset: function reset() {
-        this.state = 'beforeData';
-        this.input = '';
-
-        this.index = 0;
-        this.line = 1;
-        this.column = 0;
-
-        this.tagLine = -1;
-        this.tagColumn = -1;
-
-        this.delegate.reset();
-      },
-
-      tokenize: function tokenize(input) {
-        this.reset();
-        this.tokenizePart(input);
-        this.tokenizeEOF();
-      },
-
-      tokenizePart: function tokenizePart(input) {
-        this.input += preprocessInput(input);
-
-        while (this.index < this.input.length) {
-          this.states[this.state].call(this);
-        }
-      },
-
-      tokenizeEOF: function tokenizeEOF() {
-        this.flushData();
-      },
-
-      flushData: function flushData() {
-        if (this.state === 'data') {
-          this.delegate.finishData();
-          this.state = 'beforeData';
-        }
-      },
-
-      peek: function peek() {
-        return this.input.charAt(this.index);
-      },
-
-      consume: function consume() {
-        var char = this.peek();
-
-        this.index++;
-
-        if (char === "\n") {
-          this.line++;
-          this.column = 0;
-        } else {
-          this.column++;
-        }
-
-        return char;
-      },
-
-      consumeCharRef: function consumeCharRef() {
-        var endIndex = this.input.indexOf(';', this.index);
-        if (endIndex === -1) {
-          return;
-        }
-        var entity = this.input.slice(this.index, endIndex);
-        var chars = this.entityParser.parse(entity);
-        if (chars) {
-          this.index = endIndex + 1;
-          return chars;
-        }
-      },
-
-      markTagStart: function markTagStart() {
-        this.tagLine = this.line;
-        this.tagColumn = this.column;
-      },
-
-      states: {
-        beforeData: function beforeData() {
-          var char = this.peek();
-
-          if (char === "<") {
-            this.state = 'tagOpen';
-            this.markTagStart();
-            this.consume();
-          } else {
-            this.state = 'data';
-            this.delegate.beginData();
-          }
-        },
-
-        data: function data() {
-          var char = this.peek();
-
-          if (char === "<") {
-            this.delegate.finishData();
-            this.state = 'tagOpen';
-            this.markTagStart();
-            this.consume();
-          } else if (char === "&") {
-            this.consume();
-            this.delegate.appendToData(this.consumeCharRef() || "&");
-          } else {
-            this.consume();
-            this.delegate.appendToData(char);
-          }
-        },
-
-        tagOpen: function tagOpen() {
-          var char = this.consume();
-
-          if (char === "!") {
-            this.state = 'markupDeclaration';
-          } else if (char === "/") {
-            this.state = 'endTagOpen';
-          } else if (isAlpha(char)) {
-            this.state = 'tagName';
-            this.delegate.beginStartTag();
-            this.delegate.appendToTagName(char.toLowerCase());
-          }
-        },
-
-        markupDeclaration: function markupDeclaration() {
-          var char = this.consume();
-
-          if (char === "-" && this.input.charAt(this.index) === "-") {
-            this.index++;
-            this.state = 'commentStart';
-            this.delegate.beginComment();
-          }
-        },
-
-        commentStart: function commentStart() {
-          var char = this.consume();
-
-          if (char === "-") {
-            this.state = 'commentStartDash';
-          } else if (char === ">") {
-            this.delegate.finishComment();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToCommentData(char);
-            this.state = 'comment';
-          }
-        },
-
-        commentStartDash: function commentStartDash() {
-          var char = this.consume();
-
-          if (char === "-") {
-            this.state = 'commentEnd';
-          } else if (char === ">") {
-            this.delegate.finishComment();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToCommentData("-");
-            this.state = 'comment';
-          }
-        },
-
-        comment: function comment() {
-          var char = this.consume();
-
-          if (char === "-") {
-            this.state = 'commentEndDash';
-          } else {
-            this.delegate.appendToCommentData(char);
-          }
-        },
-
-        commentEndDash: function commentEndDash() {
-          var char = this.consume();
-
-          if (char === "-") {
-            this.state = 'commentEnd';
-          } else {
-            this.delegate.appendToCommentData("-" + char);
-            this.state = 'comment';
-          }
-        },
-
-        commentEnd: function commentEnd() {
-          var char = this.consume();
-
-          if (char === ">") {
-            this.delegate.finishComment();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToCommentData("--" + char);
-            this.state = 'comment';
-          }
-        },
-
-        tagName: function tagName() {
-          var char = this.consume();
-
-          if (isSpace(char)) {
-            this.state = 'beforeAttributeName';
-          } else if (char === "/") {
-            this.state = 'selfClosingStartTag';
-          } else if (char === ">") {
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToTagName(char);
-          }
-        },
-
-        beforeAttributeName: function beforeAttributeName() {
-          var char = this.consume();
-
-          if (isSpace(char)) {
-            return;
-          } else if (char === "/") {
-            this.state = 'selfClosingStartTag';
-          } else if (char === ">") {
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.state = 'attributeName';
-            this.delegate.beginAttribute();
-            this.delegate.appendToAttributeName(char);
-          }
-        },
-
-        attributeName: function attributeName() {
-          var char = this.consume();
-
-          if (isSpace(char)) {
-            this.state = 'afterAttributeName';
-          } else if (char === "/") {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.state = 'selfClosingStartTag';
-          } else if (char === "=") {
-            this.state = 'beforeAttributeValue';
-          } else if (char === ">") {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToAttributeName(char);
-          }
-        },
-
-        afterAttributeName: function afterAttributeName() {
-          var char = this.consume();
-
-          if (isSpace(char)) {
-            return;
-          } else if (char === "/") {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.state = 'selfClosingStartTag';
-          } else if (char === "=") {
-            this.state = 'beforeAttributeValue';
-          } else if (char === ">") {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.state = 'attributeName';
-            this.delegate.beginAttribute();
-            this.delegate.appendToAttributeName(char);
-          }
-        },
-
-        beforeAttributeValue: function beforeAttributeValue() {
-          var char = this.consume();
-
-          if (isSpace(char)) {} else if (char === '"') {
-            this.state = 'attributeValueDoubleQuoted';
-            this.delegate.beginAttributeValue(true);
-          } else if (char === "'") {
-            this.state = 'attributeValueSingleQuoted';
-            this.delegate.beginAttributeValue(true);
-          } else if (char === ">") {
-            this.delegate.beginAttributeValue(false);
-            this.delegate.finishAttributeValue();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.state = 'attributeValueUnquoted';
-            this.delegate.beginAttributeValue(false);
-            this.delegate.appendToAttributeValue(char);
-          }
-        },
-
-        attributeValueDoubleQuoted: function attributeValueDoubleQuoted() {
-          var char = this.consume();
-
-          if (char === '"') {
-            this.delegate.finishAttributeValue();
-            this.state = 'afterAttributeValueQuoted';
-          } else if (char === "&") {
-            this.delegate.appendToAttributeValue(this.consumeCharRef('"') || "&");
-          } else {
-            this.delegate.appendToAttributeValue(char);
-          }
-        },
-
-        attributeValueSingleQuoted: function attributeValueSingleQuoted() {
-          var char = this.consume();
-
-          if (char === "'") {
-            this.delegate.finishAttributeValue();
-            this.state = 'afterAttributeValueQuoted';
-          } else if (char === "&") {
-            this.delegate.appendToAttributeValue(this.consumeCharRef("'") || "&");
-          } else {
-            this.delegate.appendToAttributeValue(char);
-          }
-        },
-
-        attributeValueUnquoted: function attributeValueUnquoted() {
-          var char = this.consume();
-
-          if (isSpace(char)) {
-            this.delegate.finishAttributeValue();
-            this.state = 'beforeAttributeName';
-          } else if (char === "&") {
-            this.delegate.appendToAttributeValue(this.consumeCharRef(">") || "&");
-          } else if (char === ">") {
-            this.delegate.finishAttributeValue();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.delegate.appendToAttributeValue(char);
-          }
-        },
-
-        afterAttributeValueQuoted: function afterAttributeValueQuoted() {
-          var char = this.peek();
-
-          if (isSpace(char)) {
-            this.consume();
-            this.state = 'beforeAttributeName';
-          } else if (char === "/") {
-            this.consume();
-            this.state = 'selfClosingStartTag';
-          } else if (char === ">") {
-            this.consume();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.state = 'beforeAttributeName';
-          }
-        },
-
-        selfClosingStartTag: function selfClosingStartTag() {
-          var char = this.peek();
-
-          if (char === ">") {
-            this.consume();
-            this.delegate.markTagAsSelfClosing();
-            this.delegate.finishTag();
-            this.state = 'beforeData';
-          } else {
-            this.state = 'beforeAttributeName';
-          }
-        },
-
-        endTagOpen: function endTagOpen() {
-          var char = this.consume();
-
-          if (isAlpha(char)) {
-            this.state = 'tagName';
-            this.delegate.beginEndTag();
-            this.delegate.appendToTagName(char.toLowerCase());
-          }
-        }
-      }
-    };
-
-    function Tokenizer(entityParser, options) {
-      this.token = null;
-      this.startLine = 1;
-      this.startColumn = 0;
-      this.options = options || {};
-      this.tokenizer = new EventedTokenizer(this, entityParser);
-    }
-
-    Tokenizer.prototype = {
-      tokenize: function tokenize(input) {
-        this.tokens = [];
-        this.tokenizer.tokenize(input);
-        return this.tokens;
-      },
-
-      tokenizePart: function tokenizePart(input) {
-        this.tokens = [];
-        this.tokenizer.tokenizePart(input);
-        return this.tokens;
-      },
-
-      tokenizeEOF: function tokenizeEOF() {
-        this.tokens = [];
-        this.tokenizer.tokenizeEOF();
-        return this.tokens[0];
-      },
-
-      reset: function reset() {
-        this.token = null;
-        this.startLine = 1;
-        this.startColumn = 0;
-      },
-
-      addLocInfo: function addLocInfo() {
-        if (this.options.loc) {
-          this.token.loc = {
-            start: {
-              line: this.startLine,
-              column: this.startColumn
-            },
-            end: {
-              line: this.tokenizer.line,
-              column: this.tokenizer.column
-            }
-          };
-        }
-        this.startLine = this.tokenizer.line;
-        this.startColumn = this.tokenizer.column;
-      },
-
-      // Data
-
-      beginData: function beginData() {
-        this.token = {
-          type: 'Chars',
-          chars: ''
-        };
-        this.tokens.push(this.token);
-      },
-
-      appendToData: function appendToData(char) {
-        this.token.chars += char;
-      },
-
-      finishData: function finishData() {
-        this.addLocInfo();
-      },
-
-      // Comment
-
-      beginComment: function beginComment() {
-        this.token = {
-          type: 'Comment',
-          chars: ''
-        };
-        this.tokens.push(this.token);
-      },
-
-      appendToCommentData: function appendToCommentData(char) {
-        this.token.chars += char;
-      },
-
-      finishComment: function finishComment() {
-        this.addLocInfo();
-      },
-
-      // Tags - basic
-
-      beginStartTag: function beginStartTag() {
-        this.token = {
-          type: 'StartTag',
-          tagName: '',
-          attributes: [],
-          selfClosing: false
-        };
-        this.tokens.push(this.token);
-      },
-
-      beginEndTag: function beginEndTag() {
-        this.token = {
-          type: 'EndTag',
-          tagName: ''
-        };
-        this.tokens.push(this.token);
-      },
-
-      finishTag: function finishTag() {
-        this.addLocInfo();
-      },
-
-      markTagAsSelfClosing: function markTagAsSelfClosing() {
-        this.token.selfClosing = true;
-      },
-
-      // Tags - name
-
-      appendToTagName: function appendToTagName(char) {
-        this.token.tagName += char;
-      },
-
-      // Tags - attributes
-
-      beginAttribute: function beginAttribute() {
-        this._currentAttribute = ["", "", null];
-        this.token.attributes.push(this._currentAttribute);
-      },
-
-      appendToAttributeName: function appendToAttributeName(char) {
-        this._currentAttribute[0] += char;
-      },
-
-      beginAttributeValue: function beginAttributeValue(isQuoted) {
-        this._currentAttribute[2] = isQuoted;
-      },
-
-      appendToAttributeValue: function appendToAttributeValue(char) {
-        this._currentAttribute[1] = this._currentAttribute[1] || "";
-        this._currentAttribute[1] += char;
-      },
-
-      finishAttributeValue: function finishAttributeValue() {}
-    };
-
-    function tokenize(input, options) {
-      var tokenizer = new Tokenizer(new EntityParser(HTML5NamedCharRefs), options);
-      return tokenizer.tokenize(input);
-    }
-
-    var HTML5Tokenizer = {
-      HTML5NamedCharRefs: HTML5NamedCharRefs,
-      EntityParser: EntityParser,
-      EventedTokenizer: EventedTokenizer,
-      Tokenizer: Tokenizer,
-      tokenize: tokenize
-    };
-
-    var options = linkify.options;
-    var Options = options.Options;
-
-
-    var StartTag = 'StartTag';
-    var EndTag = 'EndTag';
-    var Chars = 'Chars';
-    var Comment = 'Comment';
-
-    /**
-    	`tokens` and `token` in this section refer to tokens generated by the HTML
-    	parser.
-    */
-    function linkifyHtml(str) {
-      var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
-
-      var tokens = HTML5Tokenizer.tokenize(str);
-      var linkifiedTokens = [];
-      var linkified = [];
-      var i;
-
-      opts = new Options(opts);
-
-      // Linkify the tokens given by the parser
-      for (i = 0; i < tokens.length; i++) {
-        var token = tokens[i];
-
-        if (token.type === StartTag) {
-          linkifiedTokens.push(token);
-
-          // Ignore all the contents of ignored tags
-          var tagName = token.tagName.toUpperCase();
-          var isIgnored = tagName === 'A' || options.contains(opts.ignoreTags, tagName);
-          if (!isIgnored) {
-            continue;
-          }
-
-          var preskipLen = linkifiedTokens.length;
-          skipTagTokens(tagName, tokens, ++i, linkifiedTokens);
-          i += linkifiedTokens.length - preskipLen - 1;
-          continue;
-        } else if (token.type !== Chars) {
-          // Skip this token, it's not important
-          linkifiedTokens.push(token);
-          continue;
-        }
-
-        // Valid text token, linkify it!
-        var linkifedChars = linkifyChars(token.chars, opts);
-        linkifiedTokens.push.apply(linkifiedTokens, linkifedChars);
-      }
-
-      // Convert the tokens back into a string
-      for (i = 0; i < linkifiedTokens.length; i++) {
-        var _token = linkifiedTokens[i];
-        switch (_token.type) {
-          case StartTag:
-            var link = '<' + _token.tagName;
-            if (_token.attributes.length > 0) {
-              var attrs = attrsToStrings(_token.attributes);
-              link += ' ' + attrs.join(' ');
-            }
-            link += '>';
-            linkified.push(link);
-            break;
-          case EndTag:
-            linkified.push('</' + _token.tagName + '>');
-            break;
-          case Chars:
-            linkified.push(escapeText(_token.chars));
-            break;
-          case Comment:
-            linkified.push('<!--' + escapeText(_token.chars) + '-->');
-            break;
-        }
-      }
-
-      return linkified.join('');
-    }
-
-    /**
-    	`tokens` and `token` in this section referes to tokens returned by
-    	`linkify.tokenize`. `linkified` will contain HTML Parser-style tokens
-    */
-    function linkifyChars(str, opts) {
-      var tokens = linkify.tokenize(str);
-      var result = [];
-
-      for (var i = 0; i < tokens.length; i++) {
-        var token = tokens[i];
-
-        if (token.type === 'nl' && opts.nl2br) {
-          result.push({
-            type: StartTag,
-            tagName: 'br',
-            attributes: [],
-            selfClosing: true
-          });
-          continue;
-        } else if (!token.isLink || !opts.check(token)) {
-          result.push({ type: Chars, chars: token.toString() });
-          continue;
-        }
-
-        var _opts$resolve = opts.resolve(token);
-
-        var href = _opts$resolve.href;
-        var formatted = _opts$resolve.formatted;
-        var formattedHref = _opts$resolve.formattedHref;
-        var tagName = _opts$resolve.tagName;
-        var className = _opts$resolve.className;
-        var target = _opts$resolve.target;
-        var attributes = _opts$resolve.attributes;
-
-        // Build up attributes
-
-        var attributeArray = [['href', formattedHref]];
-
-        if (className) {
-          attributeArray.push(['class', className]);
-        }
-
-        if (target) {
-          attributeArray.push(['target', target]);
-        }
-
-        for (var attr in attributes) {
-          attributeArray.push([attr, attributes[attr]]);
-        }
-
-        // Add the required tokens
-        result.push({
-          type: StartTag,
-          tagName: tagName,
-          attributes: attributeArray,
-          selfClosing: false
-        });
-        result.push({ type: Chars, chars: formatted });
-        result.push({ type: EndTag, tagName: tagName });
-      }
-
-      return result;
-    }
-
-    /**
-    	Returns a list of tokens skipped until the closing tag of tagName.
-
-    	* `tagName` is the closing tag which will prompt us to stop skipping
-    	* `tokens` is the array of tokens generated by HTML5Tokenizer which
-    	* `i` is the index immediately after the opening tag to skip
-    	* `skippedTokens` is an array which skipped tokens are being pushed into
-
-    	Caveats
-
-    	* Assumes that i is the first token after the given opening tagName
-    	* The closing tag will be skipped, but nothing after it
-    	* Will track whether there is a nested tag of the same type
-    */
-    function skipTagTokens(tagName, tokens, i, skippedTokens) {
-
-      // number of tokens of this type on the [fictional] stack
-      var stackCount = 1;
-
-      while (i < tokens.length && stackCount > 0) {
-        var token = tokens[i];
-        if (token.type === StartTag && token.tagName.toUpperCase() === tagName) {
-          // Nested tag of the same type, "add to stack"
-          stackCount++;
-        } else if (token.type === EndTag && token.tagName.toUpperCase() === tagName) {
-          // Closing tag
-          stackCount--;
-        }
-        skippedTokens.push(token);
-        i++;
-      }
-
-      // Note that if stackCount > 0 here, the HTML is probably invalid
-      return skippedTokens;
-    }
-
-    function escapeText(text) {
-      // Not required, HTML tokenizer ensures this occurs properly
-      return text;
-    }
-
-    function escapeAttr(attr) {
-      return attr.replace(/"/g, '&quot;');
-    }
-
-    function attrsToStrings(attrs) {
-      var attrStrs = [];
-      for (var i = 0; i < attrs.length; i++) {
-        var _attrs$i = attrs[i];
-        var name = _attrs$i[0];
-        var value = _attrs$i[1];
-
-        attrStrs.push(name + '="' + escapeAttr(value) + '"');
-      }
-      return attrStrs;
-    }
-
-    return linkifyHtml;
-  }(linkify);
-  window.linkifyHtml = linkifyHtml;
-})(window, linkify);
diff --git a/web/linkify-string.js b/web/linkify-string.js
deleted file mode 100644
index 699a94d..0000000
--- a/web/linkify-string.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (c) 2016 SoapBox Innovations Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
-*/
-
-'use strict';
-
-;(function (window, linkify) {
-	var linkifyString = function (linkify) {
-		'use strict';
-
-		/**
-  	Convert strings of text into linkable HTML text
-  */
-
-		var tokenize = linkify.tokenize;
-		var options = linkify.options;
-		var Options = options.Options;
-
-
-		function escapeText(text) {
-			return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-		}
-
-		function escapeAttr(href) {
-			return href.replace(/"/g, '&quot;');
-		}
-
-		function attributesToString(attributes) {
-			if (!attributes) {
-				return '';
-			}
-			var result = [];
-
-			for (var attr in attributes) {
-				var val = attributes[attr] + '';
-				result.push(attr + '="' + escapeAttr(val) + '"');
-			}
-			return result.join(' ');
-		}
-
-		function linkifyStr(str) {
-			var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
-
-			opts = new Options(opts);
-
-			var tokens = tokenize(str);
-			var result = [];
-
-			for (var i = 0; i < tokens.length; i++) {
-				var token = tokens[i];
-
-				if (token.type === 'nl' && opts.nl2br) {
-					result.push('<br>\n');
-					continue;
-				} else if (!token.isLink || !opts.check(token)) {
-					result.push(escapeText(token.toString()));
-					continue;
-				}
-
-				var _opts$resolve = opts.resolve(token);
-
-				var formatted = _opts$resolve.formatted;
-				var formattedHref = _opts$resolve.formattedHref;
-				var tagName = _opts$resolve.tagName;
-				var className = _opts$resolve.className;
-				var target = _opts$resolve.target;
-				var attributes = _opts$resolve.attributes;
-
-
-				var link = '<' + tagName + ' href="' + escapeAttr(formattedHref) + '"';
-
-				if (className) {
-					link += ' class="' + escapeAttr(className) + '"';
-				}
-
-				if (target) {
-					link += ' target="' + escapeAttr(target) + '"';
-				}
-
-				if (attributes) {
-					link += ' ' + attributesToString(attributes);
-				}
-
-				link += '>' + escapeText(formatted) + '</' + tagName + '>';
-				result.push(link);
-			}
-
-			return result.join('');
-		}
-
-		if (!String.prototype.linkify) {
-			String.prototype.linkify = function (opts) {
-				return linkifyStr(this, opts);
-			};
-		}
-
-		return linkifyStr;
-	}(linkify);
-	window.linkifyStr = linkifyString;
-})(window, linkify);
diff --git a/web/linkify.js b/web/linkify.js
deleted file mode 100644
index 15dc03b..0000000
--- a/web/linkify.js
+++ /dev/null
@@ -1,1271 +0,0 @@
-/*
- * Copyright (c) 2016 SoapBox Innovations Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
-*/
-
-;(function () {
-'use strict';
-
-var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
-
-(function (exports) {
-	'use strict';
-
-	function inherits(parent, child) {
-		var props = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
-
-		var extended = Object.create(parent.prototype);
-		for (var p in props) {
-			extended[p] = props[p];
-		}
-		extended.constructor = child;
-		child.prototype = extended;
-		return child;
-	}
-
-	var defaults = {
-		defaultProtocol: 'http',
-		events: null,
-		format: noop,
-		formatHref: noop,
-		nl2br: false,
-		tagName: 'a',
-		target: typeToTarget,
-		validate: true,
-		ignoreTags: [],
-		attributes: null,
-		className: 'linkified' };
-
-	function Options(opts) {
-		opts = opts || {};
-
-		this.defaultProtocol = opts.defaultProtocol || defaults.defaultProtocol;
-		this.events = opts.events || defaults.events;
-		this.format = opts.format || defaults.format;
-		this.formatHref = opts.formatHref || defaults.formatHref;
-		this.nl2br = opts.nl2br || defaults.nl2br;
-		this.tagName = opts.tagName || defaults.tagName;
-		this.target = opts.target || defaults.target;
-		this.validate = opts.validate || defaults.validate;
-		this.ignoreTags = [];
-
-		// linkAttributes and linkClass is deprecated
-		this.attributes = opts.attributes || opts.linkAttributes || defaults.attributes;
-		this.className = opts.className || opts.linkClass || defaults.className;
-
-		// Make all tags names upper case
-
-		var ignoredTags = opts.ignoreTags || defaults.ignoreTags;
-		for (var i = 0; i < ignoredTags.length; i++) {
-			this.ignoreTags.push(ignoredTags[i].toUpperCase());
-		}
-	}
-
-	Options.prototype = {
-		/**
-   * Given the token, return all options for how it should be displayed
-   */
-		resolve: function resolve(token) {
-			var href = token.toHref(this.defaultProtocol);
-			return {
-				formatted: this.get('format', token.toString(), token),
-				formattedHref: this.get('formatHref', href, token),
-				tagName: this.get('tagName', href, token),
-				className: this.get('className', href, token),
-				target: this.get('target', href, token),
-				events: this.getObject('events', href, token),
-				attributes: this.getObject('attributes', href, token)
-			};
-		},
-
-
-		/**
-   * Returns true or false based on whether a token should be displayed as a
-   * link based on the user options. By default,
-   */
-		check: function check(token) {
-			return this.get('validate', token.toString(), token);
-		},
-
-
-		// Private methods
-
-		/**
-   * Resolve an option's value based on the value of the option and the given
-   * params.
-   * @param [String] key Name of option to use
-   * @param operator will be passed to the target option if it's method
-   * @param [MultiToken] token The token from linkify.tokenize
-   */
-		get: function get(key, operator, token) {
-			var option = this[key];
-
-			if (!option) {
-				return option;
-			}
-
-			switch (typeof option === 'undefined' ? 'undefined' : _typeof(option)) {
-				case 'function':
-					return option(operator, token.type);
-				case 'object':
-					var optionValue = option[token.type] || defaults[key];
-					return typeof optionValue === 'function' ? optionValue(operator, token.type) : optionValue;
-			}
-
-			return option;
-		},
-		getObject: function getObject(key, operator, token) {
-			var option = this[key];
-			return typeof option === 'function' ? option(operator, token.type) : option;
-		}
-	};
-
-	/**
-  * Quick indexOf replacement for checking the ignoreTags option
-  */
-	function contains(arr, value) {
-		for (var i = 0; i < arr.length; i++) {
-			if (arr[i] === value) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	function noop(val) {
-		return val;
-	}
-
-	function typeToTarget(href, type) {
-		return type === 'url' ? '_blank' : null;
-	}
-
-	var options = Object.freeze({
-		defaults: defaults,
-		Options: Options,
-		contains: contains
-	});
-
-	function createStateClass() {
-		return function (tClass) {
-			this.j = [];
-			this.T = tClass || null;
-		};
-	}
-
-	/**
- 	A simple state machine that can emit token classes
-
- 	The `j` property in this class refers to state jumps. It's a
- 	multidimensional array where for each element:
-
- 	* index [0] is a symbol or class of symbols to transition to.
- 	* index [1] is a State instance which matches
-
- 	The type of symbol will depend on the target implementation for this class.
- 	In Linkify, we have a two-stage scanner. Each stage uses this state machine
- 	but with a slighly different (polymorphic) implementation.
-
- 	The `T` property refers to the token class.
-
- 	TODO: Can the `on` and `next` methods be combined?
-
- 	@class BaseState
- */
-	var BaseState = createStateClass();
-	BaseState.prototype = {
-		defaultTransition: false,
-
-		/**
-  	@method constructor
-  	@param {Class} tClass Pass in the kind of token to emit if there are
-  		no jumps after this state and the state is accepting.
-  */
-
-		/**
-  	On the given symbol(s), this machine should go to the given state
-  		@method on
-  	@param {Array|Mixed} symbol
-  	@param {BaseState} state Note that the type of this state should be the
-  		same as the current instance (i.e., don't pass in a different
-  		subclass)
-  */
-		on: function on(symbol, state) {
-			if (symbol instanceof Array) {
-				for (var i = 0; i < symbol.length; i++) {
-					this.j.push([symbol[i], state]);
-				}
-				return this;
-			}
-			this.j.push([symbol, state]);
-			return this;
-		},
-
-
-		/**
-  	Given the next item, returns next state for that item
-  	@method next
-  	@param {Mixed} item Should be an instance of the symbols handled by
-  		this particular machine.
-  	@return {State} state Returns false if no jumps are available
-  */
-		next: function next(item) {
-			for (var i = 0; i < this.j.length; i++) {
-				var jump = this.j[i];
-				var symbol = jump[0]; // Next item to check for
-				var state = jump[1]; // State to jump to if items match
-
-				// compare item with symbol
-				if (this.test(item, symbol)) {
-					return state;
-				}
-			}
-
-			// Nowhere left to jump!
-			return this.defaultTransition;
-		},
-
-
-		/**
-  	Does this state accept?
-  	`true` only of `this.T` exists
-  		@method accepts
-  	@return {Boolean}
-  */
-		accepts: function accepts() {
-			return !!this.T;
-		},
-
-
-		/**
-  	Determine whether a given item "symbolizes" the symbol, where symbol is
-  	a class of items handled by this state machine.
-  		This method should be overriden in extended classes.
-  		@method test
-  	@param {Mixed} item Does this item match the given symbol?
-  	@param {Mixed} symbol
-  	@return {Boolean}
-  */
-		test: function test(item, symbol) {
-			return item === symbol;
-		},
-
-
-		/**
-  	Emit the token for this State (just return it in this case)
-  	If this emits a token, this instance is an accepting state
-  	@method emit
-  	@return {Class} T
-  */
-		emit: function emit() {
-			return this.T;
-		}
-	};
-
-	/**
- 	State machine for string-based input
-
- 	@class CharacterState
- 	@extends BaseState
- */
-	var CharacterState = inherits(BaseState, createStateClass(), {
-		/**
-  	Does the given character match the given character or regular
-  	expression?
-  		@method test
-  	@param {String} char
-  	@param {String|RegExp} charOrRegExp
-  	@return {Boolean}
-  */
-		test: function test(character, charOrRegExp) {
-			return character === charOrRegExp || charOrRegExp instanceof RegExp && charOrRegExp.test(character);
-		}
-	});
-
-	/**
- 	State machine for input in the form of TextTokens
-
- 	@class TokenState
- 	@extends BaseState
- */
-	var State = inherits(BaseState, createStateClass(), {
-
-		/**
-   * Similar to `on`, but returns the state the results in the transition from
-   * the given item
-   * @method jump
-   * @param {Mixed} item
-   * @param {Token} [token]
-   * @return state
-   */
-		jump: function jump(token) {
-			var tClass = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
-
-			var state = this.next(new token('')); // dummy temp token
-			if (state === this.defaultTransition) {
-				// Make a new state!
-				state = new this.constructor(tClass);
-				this.on(token, state);
-			} else if (tClass) {
-				state.T = tClass;
-			}
-			return state;
-		},
-
-
-		/**
-  	Is the given token an instance of the given token class?
-  		@method test
-  	@param {TextToken} token
-  	@param {Class} tokenClass
-  	@return {Boolean}
-  */
-		test: function test(token, tokenClass) {
-			return token instanceof tokenClass;
-		}
-	});
-
-	/**
- 	Given a non-empty target string, generates states (if required) for each
- 	consecutive substring of characters in str starting from the beginning of
- 	the string. The final state will have a special value, as specified in
- 	options. All other "in between" substrings will have a default end state.
-
- 	This turns the state machine into a Trie-like data structure (rather than a
- 	intelligently-designed DFA).
-
- 	Note that I haven't really tried these with any strings other than
- 	DOMAIN.
-
- 	@param {String} str
- 	@param {CharacterState} start State to jump from the first character
- 	@param {Class} endToken Token class to emit when the given string has been
- 		matched and no more jumps exist.
- 	@param {Class} defaultToken "Filler token", or which token type to emit when
- 		we don't have a full match
- 	@return {Array} list of newly-created states
- */
-	function stateify(str, start, endToken, defaultToken) {
-		var i = 0,
-		    len = str.length,
-		    state = start,
-		    newStates = [],
-		    nextState = void 0;
-
-		// Find the next state without a jump to the next character
-		while (i < len && (nextState = state.next(str[i]))) {
-			state = nextState;
-			i++;
-		}
-
-		if (i >= len) {
-			return [];
-		} // no new tokens were added
-
-		while (i < len - 1) {
-			nextState = new CharacterState(defaultToken);
-			newStates.push(nextState);
-			state.on(str[i], nextState);
-			state = nextState;
-			i++;
-		}
-
-		nextState = new CharacterState(endToken);
-		newStates.push(nextState);
-		state.on(str[len - 1], nextState);
-
-		return newStates;
-	}
-
-	function createTokenClass() {
-		return function (value) {
-			if (value) {
-				this.v = value;
-			}
-		};
-	}
-
-	/******************************************************************************
- 	Text Tokens
- 	Tokens composed of strings
- ******************************************************************************/
-
-	/**
- 	Abstract class used for manufacturing text tokens.
- 	Pass in the value this token represents
-
- 	@class TextToken
- 	@abstract
- */
-	var TextToken = createTokenClass();
-	TextToken.prototype = {
-		toString: function toString() {
-			return this.v + '';
-		}
-	};
-
-	function inheritsToken(value) {
-		var props = value ? { v: value } : {};
-		return inherits(TextToken, createTokenClass(), props);
-	}
-
-	/**
- 	A valid domain token
- 	@class DOMAIN
- 	@extends TextToken
- */
-	var DOMAIN = inheritsToken();
-
-	/**
- 	@class AT
- 	@extends TextToken
- */
-	var AT = inheritsToken('@');
-
-	/**
- 	Represents a single colon `:` character
-
- 	@class COLON
- 	@extends TextToken
- */
-	var COLON = inheritsToken(':');
-
-	/**
- 	@class DOT
- 	@extends TextToken
- */
-	var DOT = inheritsToken('.');
-
-	/**
- 	A character class that can surround the URL, but which the URL cannot begin
- 	or end with. Does not include certain English punctuation like parentheses.
-
- 	@class PUNCTUATION
- 	@extends TextToken
- */
-	var PUNCTUATION = inheritsToken();
-
-	/**
- 	The word localhost (by itself)
- 	@class LOCALHOST
- 	@extends TextToken
- */
-	var LOCALHOST = inheritsToken();
-
-	/**
- 	Newline token
- 	@class NL
- 	@extends TextToken
- */
-	var TNL = inheritsToken('\n');
-
-	/**
- 	@class NUM
- 	@extends TextToken
- */
-	var NUM = inheritsToken();
-
-	/**
- 	@class PLUS
- 	@extends TextToken
- */
-	var PLUS = inheritsToken('+');
-
-	/**
- 	@class POUND
- 	@extends TextToken
- */
-	var POUND = inheritsToken('#');
-
-	/**
- 	Represents a web URL protocol. Supported types include
-
- 	* `http:`
- 	* `https:`
- 	* `ftp:`
- 	* `ftps:`
- 	* There's Another super weird one
-
- 	@class PROTOCOL
- 	@extends TextToken
- */
-	var PROTOCOL = inheritsToken();
-
-	/**
- 	@class QUERY
- 	@extends TextToken
- */
-	var QUERY = inheritsToken('?');
-
-	/**
- 	@class SLASH
- 	@extends TextToken
- */
-	var SLASH = inheritsToken('/');
-
-	/**
- 	@class UNDERSCORE
- 	@extends TextToken
- */
-	var UNDERSCORE = inheritsToken('_');
-
-	/**
- 	One ore more non-whitespace symbol.
- 	@class SYM
- 	@extends TextToken
- */
-	var SYM = inheritsToken();
-
-	/**
- 	@class TLD
- 	@extends TextToken
- */
-	var TLD = inheritsToken();
-
-	/**
- 	Represents a string of consecutive whitespace characters
-
- 	@class WS
- 	@extends TextToken
- */
-	var WS = inheritsToken();
-
-	/**
- 	Opening/closing bracket classes
- */
-
-	var OPENBRACE = inheritsToken('{');
-	var OPENBRACKET = inheritsToken('[');
-	var OPENANGLEBRACKET = inheritsToken('<');
-	var OPENPAREN = inheritsToken('(');
-	var CLOSEBRACE = inheritsToken('}');
-	var CLOSEBRACKET = inheritsToken(']');
-	var CLOSEANGLEBRACKET = inheritsToken('>');
-	var CLOSEPAREN = inheritsToken(')');
-
-	var TOKENS = Object.freeze({
-		Base: TextToken,
-		DOMAIN: DOMAIN,
-		AT: AT,
-		COLON: COLON,
-		DOT: DOT,
-		PUNCTUATION: PUNCTUATION,
-		LOCALHOST: LOCALHOST,
-		NL: TNL,
-		NUM: NUM,
-		PLUS: PLUS,
-		POUND: POUND,
-		QUERY: QUERY,
-		PROTOCOL: PROTOCOL,
-		SLASH: SLASH,
-		UNDERSCORE: UNDERSCORE,
-		SYM: SYM,
-		TLD: TLD,
-		WS: WS,
-		OPENBRACE: OPENBRACE,
-		OPENBRACKET: OPENBRACKET,
-		OPENANGLEBRACKET: OPENANGLEBRACKET,
-		OPENPAREN: OPENPAREN,
-		CLOSEBRACE: CLOSEBRACE,
-		CLOSEBRACKET: CLOSEBRACKET,
-		CLOSEANGLEBRACKET: CLOSEANGLEBRACKET,
-		CLOSEPAREN: CLOSEPAREN
-	});
-
-	/**
- 	The scanner provides an interface that takes a string of text as input, and
- 	outputs an array of tokens instances that can be used for easy URL parsing.
-
- 	@module linkify
- 	@submodule scanner
- 	@main scanner
- */
-
-	var tlds = 'aaa|aarp|abb|abbott|abogado|ac|academy|accenture|accountant|accountants|aco|active|actor|ad|adac|ads|adult|ae|aeg|aero|af|afl|ag|agency|ai|aig|airforce|airtel|al|alibaba|alipay|allfinanz|alsace|am|amica|amsterdam|an|analytics|android|ao|apartments|app|apple|aq|aquarelle|ar|aramco|archi|army|arpa|arte|as|asia|associates|at|attorney|au|auction|audi|audio|author|auto|autos|avianca|aw|ax|axa|az|azure|ba|baidu|band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bcg|bcn|bd|be|beats|beer|bentley|berlin|best|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bms|bmw|bn|bnl|bnpparibas|bo|boats|boehringer|bom|bond|boo|book|boots|bosch|bostik|bot|boutique|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|cc|cd|ceb|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|chase|chat|cheap|chloe|christmas|chrome|church|ci|cipriani|circle|cisco|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cool|coop|corsica|country|coupon|coupons|courses|cr|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|download|drive|dubai|durban|dvag|dz|earth|eat|ec|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|fast|feedback|ferrero|fi|film|final|finance|financial|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|ford|forex|forsale|forum|foundation|fox|fr|fresenius|frl|frogans|frontier|fund|furniture|futbol|fyi|ga|gal|gallery|gallup|game|garden|gb|gbiz|gd|gdn|ge|gea|gent|genting|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|group|gs|gt|gu|gucci|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|hdfcbank|health|healthcare|help|helsinki|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hr|hsbc|ht|hu|hyundai|ibm|icbc|ice|icu|id|ie|ifm|iinet|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|investments|io|ipiranga|iq|ir|irish|is|iselect|ist|istanbul|it|itau|iwc|jaguar|java|jcb|je|jetzt|jewelry|jlc|jll|jm|jmp|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kitchen|kiwi|km|kn|koeln|komatsu|kp|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|land|landrover|lanxess|lasalle|lat|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|legal|lexus|lgbt|li|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|limited|limo|lincoln|linde|link|live|living|lixil|lk|loan|loans|local|locus|lol|london|lotte|lotto|love|lr|ls|lt|ltd|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|mba|mc|md|me|med|media|meet|melbourne|meme|memorial|men|menu|meo|mg|mh|miami|microsoft|mil|mini|mk|ml|mm|mma|mn|mo|mobi|mobily|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar|mp|mq|mr|ms|mt|mtn|mtpc|mtr|mu|museum|mutuelle|mv|mw|mx|my|mz|na|nadex|nagoya|name|natura|navy|nc|ne|nec|net|netbank|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|nikon|ninja|nissan|nl|no|nokia|norton|nowruz|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|office|okinawa|om|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|origins|osaka|otsuka|ovh|pa|page|pamperedchef|panerai|paris|pars|partners|parts|party|passagens|pe|pet|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|promo|properties|property|protection|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|re|read|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|ricoh|rio|rip|ro|rocher|rocks|rodeo|room|rs|rsvp|ru|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|saxo|sb|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scor|scot|sd|se|seat|security|seek|select|sener|services|seven|sew|sex|sexy|sfr|sg|sh|sharp|shell|shia|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|skin|sky|skype|sl|sm|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|sr|srl|st|stada|star|starhub|statefarm|statoil|stc|stcgroup|stockholm|storage|store|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|symantec|systems|sz|tab|taipei|taobao|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|team|tech|technology|tel|telecity|telefonica|temasek|tennis|tf|tg|th|thd|theater|theatre|tickets|tienda|tiffany|tips|tires|tirol|tj|tk|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tp|tr|trade|trading|training|travel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubs|ug|uk|unicom|university|uno|uol|us|uy|uz|va|vacations|vana|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|viking|villas|vin|vip|virgin|vision|vista|vistaprint|viva|vlaanderen|vn|vodka|volkswagen|vote|voting|voto|voyage|vu|vuelos|wales|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|wme|wolterskluwer|work|works|world|ws|wtc|wtf|xbox|xerox|xin|xperia|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zara|zero|zip|zm|zone|zuerich|zw'.split('|'); // macro, see gulpfile.js
-
-	var NUMBERS = '0123456789'.split('');
-	var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
-	var WHITESPACE = [' ', '\f', '\r', '\t', '\v', ' ', ' ', '᠎']; // excluding line breaks
-
-	var domainStates = []; // states that jump to DOMAIN on /[a-z0-9]/
-	var makeState = function makeState(tokenClass) {
-		return new CharacterState(tokenClass);
-	};
-
-	// Frequently used states
-	var S_START = makeState();
-	var S_NUM = makeState(NUM);
-	var S_DOMAIN = makeState(DOMAIN);
-	var S_DOMAIN_HYPHEN = makeState(); // domain followed by 1 or more hyphen characters
-	var S_WS = makeState(WS);
-
-	// States for special URL symbols
-	S_START.on('@', makeState(AT)).on('.', makeState(DOT)).on('+', makeState(PLUS)).on('#', makeState(POUND)).on('?', makeState(QUERY)).on('/', makeState(SLASH)).on('_', makeState(UNDERSCORE)).on(':', makeState(COLON)).on('{', makeState(OPENBRACE)).on('[', makeState(OPENBRACKET)).on('<', makeState(OPENANGLEBRACKET)).on('(', makeState(OPENPAREN)).on('}', makeState(CLOSEBRACE)).on(']', makeState(CLOSEBRACKET)).on('>', makeState(CLOSEANGLEBRACKET)).on(')', makeState(CLOSEPAREN)).on([',', ';', '!', '"', '\''], makeState(PUNCTUATION));
-
-	// Whitespace jumps
-	// Tokens of only non-newline whitespace are arbitrarily long
-	S_START.on('\n', makeState(TNL)).on(WHITESPACE, S_WS);
-
-	// If any whitespace except newline, more whitespace!
-	S_WS.on(WHITESPACE, S_WS);
-
-	// Generates states for top-level domains
-	// Note that this is most accurate when tlds are in alphabetical order
-	for (var i = 0; i < tlds.length; i++) {
-		var newStates = stateify(tlds[i], S_START, TLD, DOMAIN);
-		domainStates.push.apply(domainStates, newStates);
-	}
-
-	// Collect the states generated by different protocls
-	var partialProtocolFileStates = stateify('file', S_START, DOMAIN, DOMAIN);
-	var partialProtocolFtpStates = stateify('ftp', S_START, DOMAIN, DOMAIN);
-	var partialProtocolHttpStates = stateify('http', S_START, DOMAIN, DOMAIN);
-
-	// Add the states to the array of DOMAINeric states
-	domainStates.push.apply(domainStates, partialProtocolFileStates);
-	domainStates.push.apply(domainStates, partialProtocolFtpStates);
-	domainStates.push.apply(domainStates, partialProtocolHttpStates);
-
-	// Protocol states
-	var S_PROTOCOL_FILE = partialProtocolFileStates.pop();
-	var S_PROTOCOL_FTP = partialProtocolFtpStates.pop();
-	var S_PROTOCOL_HTTP = partialProtocolHttpStates.pop();
-	var S_PROTOCOL_SECURE = makeState(DOMAIN);
-	var S_FULL_PROTOCOL = makeState(PROTOCOL); // Full protocol ends with COLON
-
-	// Secure protocols (end with 's')
-	S_PROTOCOL_FTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL);
-
-	S_PROTOCOL_HTTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL);
-
-	domainStates.push(S_PROTOCOL_SECURE);
-
-	// Become protocol tokens after a COLON
-	S_PROTOCOL_FILE.on(':', S_FULL_PROTOCOL);
-	S_PROTOCOL_SECURE.on(':', S_FULL_PROTOCOL);
-
-	// Localhost
-	var partialLocalhostStates = stateify('localhost', S_START, LOCALHOST, DOMAIN);
-	domainStates.push.apply(domainStates, partialLocalhostStates);
-
-	// Everything else
-	// DOMAINs make more DOMAINs
-	// Number and character transitions
-	S_START.on(NUMBERS, S_NUM);
-	S_NUM.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_NUM).on(ALPHANUM, S_DOMAIN); // number becomes DOMAIN
-
-	S_DOMAIN.on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN);
-
-	// All the generated states should have a jump to DOMAIN
-	for (var _i = 0; _i < domainStates.length; _i++) {
-		domainStates[_i].on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN);
-	}
-
-	S_DOMAIN_HYPHEN.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_DOMAIN).on(ALPHANUM, S_DOMAIN);
-
-	// Set default transition
-	S_START.defaultTransition = makeState(SYM);
-
-	/**
- 	Given a string, returns an array of TOKEN instances representing the
- 	composition of that string.
-
- 	@method run
- 	@param {String} str Input string to scan
- 	@return {Array} Array of TOKEN instances
- */
-	var run = function run(str) {
-
-		// The state machine only looks at lowercase strings.
-		// This selective `toLowerCase` is used because lowercasing the entire
-		// string causes the length and character position to vary in some in some
-		// non-English strings. This happens only on V8-based runtimes.
-		var lowerStr = str.replace(/[A-Z]/g, function (c) {
-			return c.toLowerCase();
-		});
-		var len = str.length;
-		var tokens = []; // return value
-
-		var cursor = 0;
-
-		// Tokenize the string
-		while (cursor < len) {
-			var state = S_START;
-			var secondState = null;
-			var nextState = null;
-			var tokenLength = 0;
-			var latestAccepting = null;
-			var sinceAccepts = -1;
-
-			while (cursor < len && (nextState = state.next(lowerStr[cursor]))) {
-				secondState = null;
-				state = nextState;
-
-				// Keep track of the latest accepting state
-				if (state.accepts()) {
-					sinceAccepts = 0;
-					latestAccepting = state;
-				} else if (sinceAccepts >= 0) {
-					sinceAccepts++;
-				}
-
-				tokenLength++;
-				cursor++;
-			}
-
-			if (sinceAccepts < 0) {
-				continue;
-			} // Should never happen
-
-			// Roll back to the latest accepting state
-			cursor -= sinceAccepts;
-			tokenLength -= sinceAccepts;
-
-			// Get the class for the new token
-			var TOKEN = latestAccepting.emit(); // Current token class
-
-			// No more jumps, just make a new token
-			tokens.push(new TOKEN(str.substr(cursor - tokenLength, tokenLength)));
-		}
-
-		return tokens;
-	};
-
-	var start = S_START;
-
-	var scanner = Object.freeze({
-		State: CharacterState,
-		TOKENS: TOKENS,
-		run: run,
-		start: start
-	});
-
-	/******************************************************************************
- 	Multi-Tokens
- 	Tokens composed of arrays of TextTokens
- ******************************************************************************/
-
-	// Is the given token a valid domain token?
-	// Should nums be included here?
-	function isDomainToken(token) {
-		return token instanceof DOMAIN || token instanceof TLD;
-	}
-
-	/**
- 	Abstract class used for manufacturing tokens of text tokens. That is rather
- 	than the value for a token being a small string of text, it's value an array
- 	of text tokens.
-
- 	Used for grouping together URLs, emails, hashtags, and other potential
- 	creations.
-
- 	@class MultiToken
- 	@abstract
- */
-	var MultiToken = createTokenClass();
-
-	MultiToken.prototype = {
-		/**
-  	String representing the type for this token
-  	@property type
-  	@default 'TOKEN'
-  */
-		type: 'token',
-
-		/**
-  	Is this multitoken a link?
-  	@property isLink
-  	@default false
-  */
-		isLink: false,
-
-		/**
-  	Return the string this token represents.
-  	@method toString
-  	@return {String}
-  */
-		toString: function toString() {
-			var result = [];
-			for (var _i2 = 0; _i2 < this.v.length; _i2++) {
-				result.push(this.v[_i2].toString());
-			}
-			return result.join('');
-		},
-
-
-		/**
-  	What should the value for this token be in the `href` HTML attribute?
-  	Returns the `.toString` value by default.
-  		@method toHref
-  	@return {String}
-  */
-		toHref: function toHref() {
-			return this.toString();
-		},
-
-
-		/**
-  	Returns a hash of relevant values for this token, which includes keys
-  	* type - Kind of token ('url', 'email', etc.)
-  	* value - Original text
-  	* href - The value that should be added to the anchor tag's href
-  		attribute
-  		@method toObject
-  	@param {String} [protocol] `'http'` by default
-  	@return {Object}
-  */
-		toObject: function toObject() {
-			var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0];
-
-			return {
-				type: this.type,
-				value: this.toString(),
-				href: this.toHref(protocol)
-			};
-		}
-	};
-
-	/**
- 	Represents a list of tokens making up a valid email address
- 	@class EMAIL
- 	@extends MultiToken
- */
-	var EMAIL = inherits(MultiToken, createTokenClass(), {
-		type: 'email',
-		isLink: true,
-		toHref: function toHref() {
-			return 'mailto:' + this.toString();
-		}
-	});
-
-	/**
- 	Represents some plain text
- 	@class TEXT
- 	@extends MultiToken
- */
-	var TEXT = inherits(MultiToken, createTokenClass(), { type: 'text' });
-
-	/**
- 	Multi-linebreak token - represents a line break
- 	@class NL
- 	@extends MultiToken
- */
-	var NL = inherits(MultiToken, createTokenClass(), { type: 'nl' });
-
-	/**
- 	Represents a list of tokens making up a valid URL
- 	@class URL
- 	@extends MultiToken
- */
-	var URL = inherits(MultiToken, createTokenClass(), {
-		type: 'url',
-		isLink: true,
-
-		/**
-  	Lowercases relevant parts of the domain and adds the protocol if
-  	required. Note that this will not escape unsafe HTML characters in the
-  	URL.
-  		@method href
-  	@param {String} protocol
-  	@return {String}
-  */
-		toHref: function toHref() {
-			var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0];
-
-			var hasProtocol = false;
-			var hasSlashSlash = false;
-			var tokens = this.v;
-			var result = [];
-			var i = 0;
-
-			// Make the first part of the domain lowercase
-			// Lowercase protocol
-			while (tokens[i] instanceof PROTOCOL) {
-				hasProtocol = true;
-				result.push(tokens[i].toString().toLowerCase());
-				i++;
-			}
-
-			// Skip slash-slash
-			while (tokens[i] instanceof SLASH) {
-				hasSlashSlash = true;
-				result.push(tokens[i].toString());
-				i++;
-			}
-
-			// Lowercase all other characters in the domain
-			while (isDomainToken(tokens[i])) {
-				result.push(tokens[i].toString().toLowerCase());
-				i++;
-			}
-
-			// Leave all other characters as they were written
-			for (; i < tokens.length; i++) {
-				result.push(tokens[i].toString());
-			}
-
-			result = result.join('');
-
-			if (!(hasProtocol || hasSlashSlash)) {
-				result = protocol + '://' + result;
-			}
-
-			return result;
-		},
-		hasProtocol: function hasProtocol() {
-			return this.v[0] instanceof PROTOCOL;
-		}
-	});
-
-	var TOKENS$1 = Object.freeze({
-		Base: MultiToken,
-		EMAIL: EMAIL,
-		NL: NL,
-		TEXT: TEXT,
-		URL: URL
-	});
-
-	/**
- 	Not exactly parser, more like the second-stage scanner (although we can
- 	theoretically hotswap the code here with a real parser in the future... but
- 	for a little URL-finding utility abstract syntax trees may be a little
- 	overkill).
-
- 	URL format: http://en.wikipedia.org/wiki/URI_scheme
- 	Email format: http://en.wikipedia.org/wiki/Email_address (links to RFC in
- 	reference)
-
- 	@module linkify
- 	@submodule parser
- 	@main parser
- */
-
-	var makeState$1 = function makeState$1(tokenClass) {
-		return new State(tokenClass);
-	};
-
-	// The universal starting state.
-	var S_START$1 = makeState$1();
-
-	// Intermediate states for URLs. Note that domains that begin with a protocol
-	// are treated slighly differently from those that don't.
-	var S_PROTOCOL = makeState$1(); // e.g., 'http:'
-	var S_PROTOCOL_SLASH = makeState$1(); // e.g., '/', 'http:/''
-	var S_PROTOCOL_SLASH_SLASH = makeState$1(); // e.g., '//', 'http://'
-	var S_DOMAIN$1 = makeState$1(); // parsed string ends with a potential domain name (A)
-	var S_DOMAIN_DOT = makeState$1(); // (A) domain followed by DOT
-	var S_TLD = makeState$1(URL); // (A) Simplest possible URL with no query string
-	var S_TLD_COLON = makeState$1(); // (A) URL followed by colon (potential port number here)
-	var S_TLD_PORT = makeState$1(URL); // TLD followed by a port number
-	var S_URL = makeState$1(URL); // Long URL with optional port and maybe query string
-	var S_URL_NON_ACCEPTING = makeState$1(); // URL followed by some symbols (will not be part of the final URL)
-	var S_URL_OPENBRACE = makeState$1(); // URL followed by {
-	var S_URL_OPENBRACKET = makeState$1(); // URL followed by [
-	var S_URL_OPENANGLEBRACKET = makeState$1(); // URL followed by <
-	var S_URL_OPENPAREN = makeState$1(); // URL followed by (
-	var S_URL_OPENBRACE_Q = makeState$1(URL); // URL followed by { and some symbols that the URL can end it
-	var S_URL_OPENBRACKET_Q = makeState$1(URL); // URL followed by [ and some symbols that the URL can end it
-	var S_URL_OPENANGLEBRACKET_Q = makeState$1(URL); // URL followed by < and some symbols that the URL can end it
-	var S_URL_OPENPAREN_Q = makeState$1(URL); // URL followed by ( and some symbols that the URL can end it
-	var S_URL_OPENBRACE_SYMS = makeState$1(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it
-	var S_URL_OPENBRACKET_SYMS = makeState$1(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it
-	var S_URL_OPENANGLEBRACKET_SYMS = makeState$1(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it
-	var S_URL_OPENPAREN_SYMS = makeState$1(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it
-	var S_EMAIL_DOMAIN = makeState$1(); // parsed string starts with local email info + @ with a potential domain name (C)
-	var S_EMAIL_DOMAIN_DOT = makeState$1(); // (C) domain followed by DOT
-	var S_EMAIL = makeState$1(EMAIL); // (C) Possible email address (could have more tlds)
-	var S_EMAIL_COLON = makeState$1(); // (C) URL followed by colon (potential port number here)
-	var S_EMAIL_PORT = makeState$1(EMAIL); // (C) Email address with a port
-	var S_LOCALPART = makeState$1(); // Local part of the email address
-	var S_LOCALPART_AT = makeState$1(); // Local part of the email address plus @
-	var S_LOCALPART_DOT = makeState$1(); // Local part of the email address plus '.' (localpart cannot end in .)
-	var S_NL = makeState$1(NL); // single new line
-
-	// Make path from start to protocol (with '//')
-	S_START$1.on(TNL, S_NL).on(PROTOCOL, S_PROTOCOL).on(SLASH, S_PROTOCOL_SLASH);
-
-	S_PROTOCOL.on(SLASH, S_PROTOCOL_SLASH);
-	S_PROTOCOL_SLASH.on(SLASH, S_PROTOCOL_SLASH_SLASH);
-
-	// The very first potential domain name
-	S_START$1.on(TLD, S_DOMAIN$1).on(DOMAIN, S_DOMAIN$1).on(LOCALHOST, S_TLD).on(NUM, S_DOMAIN$1);
-
-	// Force URL for anything sane followed by protocol
-	S_PROTOCOL_SLASH_SLASH.on(TLD, S_URL).on(DOMAIN, S_URL).on(NUM, S_URL).on(LOCALHOST, S_URL);
-
-	// Account for dots and hyphens
-	// hyphens are usually parts of domain names
-	S_DOMAIN$1.on(DOT, S_DOMAIN_DOT);
-	S_EMAIL_DOMAIN.on(DOT, S_EMAIL_DOMAIN_DOT);
-
-	// Hyphen can jump back to a domain name
-
-	// After the first domain and a dot, we can find either a URL or another domain
-	S_DOMAIN_DOT.on(TLD, S_TLD).on(DOMAIN, S_DOMAIN$1).on(NUM, S_DOMAIN$1).on(LOCALHOST, S_DOMAIN$1);
-
-	S_EMAIL_DOMAIN_DOT.on(TLD, S_EMAIL).on(DOMAIN, S_EMAIL_DOMAIN).on(NUM, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL_DOMAIN);
-
-	// S_TLD accepts! But the URL could be longer, try to find a match greedily
-	// The `run` function should be able to "rollback" to the accepting state
-	S_TLD.on(DOT, S_DOMAIN_DOT);
-	S_EMAIL.on(DOT, S_EMAIL_DOMAIN_DOT);
-
-	// Become real URLs after `SLASH` or `COLON NUM SLASH`
-	// Here PSS and non-PSS converge
-	S_TLD.on(COLON, S_TLD_COLON).on(SLASH, S_URL);
-	S_TLD_COLON.on(NUM, S_TLD_PORT);
-	S_TLD_PORT.on(SLASH, S_URL);
-	S_EMAIL.on(COLON, S_EMAIL_COLON);
-	S_EMAIL_COLON.on(NUM, S_EMAIL_PORT);
-
-	// Types of characters the URL can definitely end in
-	var qsAccepting = [DOMAIN, AT, LOCALHOST, NUM, PLUS, POUND, PROTOCOL, SLASH, TLD, UNDERSCORE, SYM];
-
-	// Types of tokens that can follow a URL and be part of the query string
-	// but cannot be the very last characters
-	// Characters that cannot appear in the URL at all should be excluded
-	var qsNonAccepting = [COLON, DOT, QUERY, PUNCTUATION, CLOSEBRACE, CLOSEBRACKET, CLOSEANGLEBRACKET, CLOSEPAREN, OPENBRACE, OPENBRACKET, OPENANGLEBRACKET, OPENPAREN];
-
-	// These states are responsible primarily for determining whether or not to
-	// include the final round bracket.
-
-	// URL, followed by an opening bracket
-	S_URL.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN);
-
-	// URL with extra symbols at the end, followed by an opening bracket
-	S_URL_NON_ACCEPTING.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN);
-
-	// Closing bracket component. This character WILL be included in the URL
-	S_URL_OPENBRACE.on(CLOSEBRACE, S_URL);
-	S_URL_OPENBRACKET.on(CLOSEBRACKET, S_URL);
-	S_URL_OPENANGLEBRACKET.on(CLOSEANGLEBRACKET, S_URL);
-	S_URL_OPENPAREN.on(CLOSEPAREN, S_URL);
-	S_URL_OPENBRACE_Q.on(CLOSEBRACE, S_URL);
-	S_URL_OPENBRACKET_Q.on(CLOSEBRACKET, S_URL);
-	S_URL_OPENANGLEBRACKET_Q.on(CLOSEANGLEBRACKET, S_URL);
-	S_URL_OPENPAREN_Q.on(CLOSEPAREN, S_URL);
-	S_URL_OPENBRACE_SYMS.on(CLOSEBRACE, S_URL);
-	S_URL_OPENBRACKET_SYMS.on(CLOSEBRACKET, S_URL);
-	S_URL_OPENANGLEBRACKET_SYMS.on(CLOSEANGLEBRACKET, S_URL);
-	S_URL_OPENPAREN_SYMS.on(CLOSEPAREN, S_URL);
-
-	// URL that beings with an opening bracket, followed by a symbols.
-	// Note that the final state can still be `S_URL_OPENBRACE_Q` (if the URL only
-	// has a single opening bracket for some reason).
-	S_URL_OPENBRACE.on(qsAccepting, S_URL_OPENBRACE_Q);
-	S_URL_OPENBRACKET.on(qsAccepting, S_URL_OPENBRACKET_Q);
-	S_URL_OPENANGLEBRACKET.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q);
-	S_URL_OPENPAREN.on(qsAccepting, S_URL_OPENPAREN_Q);
-	S_URL_OPENBRACE.on(qsNonAccepting, S_URL_OPENBRACE_SYMS);
-	S_URL_OPENBRACKET.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS);
-	S_URL_OPENANGLEBRACKET.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS);
-	S_URL_OPENPAREN.on(qsNonAccepting, S_URL_OPENPAREN_SYMS);
-
-	// URL that begins with an opening bracket, followed by some symbols
-	S_URL_OPENBRACE_Q.on(qsAccepting, S_URL_OPENBRACE_Q);
-	S_URL_OPENBRACKET_Q.on(qsAccepting, S_URL_OPENBRACKET_Q);
-	S_URL_OPENANGLEBRACKET_Q.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q);
-	S_URL_OPENPAREN_Q.on(qsAccepting, S_URL_OPENPAREN_Q);
-	S_URL_OPENBRACE_Q.on(qsNonAccepting, S_URL_OPENBRACE_Q);
-	S_URL_OPENBRACKET_Q.on(qsNonAccepting, S_URL_OPENBRACKET_Q);
-	S_URL_OPENANGLEBRACKET_Q.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_Q);
-	S_URL_OPENPAREN_Q.on(qsNonAccepting, S_URL_OPENPAREN_Q);
-
-	S_URL_OPENBRACE_SYMS.on(qsAccepting, S_URL_OPENBRACE_Q);
-	S_URL_OPENBRACKET_SYMS.on(qsAccepting, S_URL_OPENBRACKET_Q);
-	S_URL_OPENANGLEBRACKET_SYMS.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q);
-	S_URL_OPENPAREN_SYMS.on(qsAccepting, S_URL_OPENPAREN_Q);
-	S_URL_OPENBRACE_SYMS.on(qsNonAccepting, S_URL_OPENBRACE_SYMS);
-	S_URL_OPENBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS);
-	S_URL_OPENANGLEBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS);
-	S_URL_OPENPAREN_SYMS.on(qsNonAccepting, S_URL_OPENPAREN_SYMS);
-
-	// Account for the query string
-	S_URL.on(qsAccepting, S_URL);
-	S_URL_NON_ACCEPTING.on(qsAccepting, S_URL);
-
-	S_URL.on(qsNonAccepting, S_URL_NON_ACCEPTING);
-	S_URL_NON_ACCEPTING.on(qsNonAccepting, S_URL_NON_ACCEPTING);
-
-	// Email address-specific state definitions
-	// Note: We are not allowing '/' in email addresses since this would interfere
-	// with real URLs
-
-	// Tokens allowed in the localpart of the email
-	var localpartAccepting = [DOMAIN, NUM, PLUS, POUND, QUERY, UNDERSCORE, SYM, TLD];
-
-	// Some of the tokens in `localpartAccepting` are already accounted for here and
-	// will not be overwritten (don't worry)
-	S_DOMAIN$1.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT);
-	S_TLD.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT);
-	S_DOMAIN_DOT.on(localpartAccepting, S_LOCALPART);
-
-	// Okay we're on a localpart. Now what?
-	// TODO: IP addresses and what if the email starts with numbers?
-	S_LOCALPART.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT) // close to an email address now
-	.on(DOT, S_LOCALPART_DOT);
-	S_LOCALPART_DOT.on(localpartAccepting, S_LOCALPART);
-	S_LOCALPART_AT.on(TLD, S_EMAIL_DOMAIN).on(DOMAIN, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL);
-	// States following `@` defined above
-
-	var run$1 = function run$1(tokens) {
-		var len = tokens.length;
-		var cursor = 0;
-		var multis = [];
-		var textTokens = [];
-
-		while (cursor < len) {
-			var state = S_START$1;
-			var secondState = null;
-			var nextState = null;
-			var multiLength = 0;
-			var latestAccepting = null;
-			var sinceAccepts = -1;
-
-			while (cursor < len && !(secondState = state.next(tokens[cursor]))) {
-				// Starting tokens with nowhere to jump to.
-				// Consider these to be just plain text
-				textTokens.push(tokens[cursor++]);
-			}
-
-			while (cursor < len && (nextState = secondState || state.next(tokens[cursor]))) {
-
-				// Get the next state
-				secondState = null;
-				state = nextState;
-
-				// Keep track of the latest accepting state
-				if (state.accepts()) {
-					sinceAccepts = 0;
-					latestAccepting = state;
-				} else if (sinceAccepts >= 0) {
-					sinceAccepts++;
-				}
-
-				cursor++;
-				multiLength++;
-			}
-
-			if (sinceAccepts < 0) {
-
-				// No accepting state was found, part of a regular text token
-				// Add all the tokens we looked at to the text tokens array
-				for (var _i3 = cursor - multiLength; _i3 < cursor; _i3++) {
-					textTokens.push(tokens[_i3]);
-				}
-			} else {
-
-				// Accepting state!
-
-				// First close off the textTokens (if available)
-				if (textTokens.length > 0) {
-					multis.push(new TEXT(textTokens));
-					textTokens = [];
-				}
-
-				// Roll back to the latest accepting state
-				cursor -= sinceAccepts;
-				multiLength -= sinceAccepts;
-
-				// Create a new multitoken
-				var MULTI = latestAccepting.emit();
-				multis.push(new MULTI(tokens.slice(cursor - multiLength, cursor)));
-			}
-		}
-
-		// Finally close off the textTokens (if available)
-		if (textTokens.length > 0) {
-			multis.push(new TEXT(textTokens));
-		}
-
-		return multis;
-	};
-
-	var parser = Object.freeze({
-		State: State,
-		TOKENS: TOKENS$1,
-		run: run$1,
-		start: S_START$1
-	});
-
-	if (!Array.isArray) {
-		Array.isArray = function (arg) {
-			return Object.prototype.toString.call(arg) === '[object Array]';
-		};
-	}
-
-	/**
- 	Converts a string into tokens that represent linkable and non-linkable bits
- 	@method tokenize
- 	@param {String} str
- 	@return {Array} tokens
- */
-	var tokenize = function tokenize(str) {
-		return run$1(run(str));
-	};
-
-	/**
- 	Returns a list of linkable items in the given string.
- */
-	var find = function find(str) {
-		var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
-
-		var tokens = tokenize(str);
-		var filtered = [];
-
-		for (var i = 0; i < tokens.length; i++) {
-			var token = tokens[i];
-			if (token.isLink && (!type || token.type === type)) {
-				filtered.push(token.toObject());
-			}
-		}
-
-		return filtered;
-	};
-
-	/**
- 	Is the given string valid linkable text of some sort
- 	Note that this does not trim the text for you.
-
- 	Optionally pass in a second `type` param, which is the type of link to test
- 	for.
-
- 	For example,
-
- 		test(str, 'email');
-
- 	Will return `true` if str is a valid email.
- */
-	var test = function test(str) {
-		var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
-
-		var tokens = tokenize(str);
-		return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].type === type);
-	};
-
-	exports.find = find;
-	exports.inherits = inherits;
-	exports.options = options;
-	exports.parser = parser;
-	exports.scanner = scanner;
-	exports.test = test;
-	exports.tokenize = tokenize;
-})(window.linkify = window.linkify || {});
-})();
diff --git a/web/qwebchannel.js b/web/qwebchannel.js
deleted file mode 100644
index abf8461..0000000
--- a/web/qwebchannel.js
+++ /dev/null
@@ -1,427 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2016 The Qt Company Ltd.
-** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the QtWebChannel module of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:LGPL$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 3 as published by the Free Software
-** Foundation and appearing in the file LICENSE.LGPL3 included in the
-** packaging of this file. Please review the following information to
-** ensure the GNU Lesser General Public License version 3 requirements
-** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 2.0 or (at your option) the GNU General
-** Public license version 3 or any later version approved by the KDE Free
-** Qt Foundation. The licenses are as published by the Free Software
-** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-2.0.html and
-** https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
-
-"use strict";
-
-var QWebChannelMessageTypes = {
-    signal: 1,
-    propertyUpdate: 2,
-    init: 3,
-    idle: 4,
-    debug: 5,
-    invokeMethod: 6,
-    connectToSignal: 7,
-    disconnectFromSignal: 8,
-    setProperty: 9,
-    response: 10,
-};
-
-var QWebChannel = function(transport, initCallback)
-{
-    if (typeof transport !== "object" || typeof transport.send !== "function") {
-        console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
-                      " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
-        return;
-    }
-
-    var channel = this;
-    this.transport = transport;
-
-    this.send = function(data)
-    {
-        if (typeof(data) !== "string") {
-            data = JSON.stringify(data);
-        }
-        channel.transport.send(data);
-    }
-
-    this.transport.onmessage = function(message)
-    {
-        var data = message.data;
-        if (typeof data === "string") {
-            data = JSON.parse(data);
-        }
-        switch (data.type) {
-            case QWebChannelMessageTypes.signal:
-                channel.handleSignal(data);
-                break;
-            case QWebChannelMessageTypes.response:
-                channel.handleResponse(data);
-                break;
-            case QWebChannelMessageTypes.propertyUpdate:
-                channel.handlePropertyUpdate(data);
-                break;
-            default:
-                console.error("invalid message received:", message.data);
-                break;
-        }
-    }
-
-    this.execCallbacks = {};
-    this.execId = 0;
-    this.exec = function(data, callback)
-    {
-        if (!callback) {
-            // if no callback is given, send directly
-            channel.send(data);
-            return;
-        }
-        if (channel.execId === Number.MAX_VALUE) {
-            // wrap
-            channel.execId = Number.MIN_VALUE;
-        }
-        if (data.hasOwnProperty("id")) {
-            console.error("Cannot exec message with property id: " + JSON.stringify(data));
-            return;
-        }
-        data.id = channel.execId++;
-        channel.execCallbacks[data.id] = callback;
-        channel.send(data);
-    };
-
-    this.objects = {};
-
-    this.handleSignal = function(message)
-    {
-        var object = channel.objects[message.object];
-        if (object) {
-            object.signalEmitted(message.signal, message.args);
-        } else {
-            console.warn("Unhandled signal: " + message.object + "::" + message.signal);
-        }
-    }
-
-    this.handleResponse = function(message)
-    {
-        if (!message.hasOwnProperty("id")) {
-            console.error("Invalid response message received: ", JSON.stringify(message));
-            return;
-        }
-        channel.execCallbacks[message.id](message.data);
-        delete channel.execCallbacks[message.id];
-    }
-
-    this.handlePropertyUpdate = function(message)
-    {
-        for (var i in message.data) {
-            var data = message.data[i];
-            var object = channel.objects[data.object];
-            if (object) {
-                object.propertyUpdate(data.signals, data.properties);
-            } else {
-                console.warn("Unhandled property update: " + data.object + "::" + data.signal);
-            }
-        }
-        channel.exec({type: QWebChannelMessageTypes.idle});
-    }
-
-    this.debug = function(message)
-    {
-        channel.send({type: QWebChannelMessageTypes.debug, data: message});
-    };
-
-    channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
-        for (var objectName in data) {
-            var object = new QObject(objectName, data[objectName], channel);
-        }
-        // now unwrap properties, which might reference other registered objects
-        for (var objectName in channel.objects) {
-            channel.objects[objectName].unwrapProperties();
-        }
-        if (initCallback) {
-            initCallback(channel);
-        }
-        channel.exec({type: QWebChannelMessageTypes.idle});
-    });
-};
-
-function QObject(name, data, webChannel)
-{
-    this.__id__ = name;
-    webChannel.objects[name] = this;
-
-    // List of callbacks that get invoked upon signal emission
-    this.__objectSignals__ = {};
-
-    // Cache of all properties, updated when a notify signal is emitted
-    this.__propertyCache__ = {};
-
-    var object = this;
-
-    // ----------------------------------------------------------------------
-
-    this.unwrapQObject = function(response)
-    {
-        if (response instanceof Array) {
-            // support list of objects
-            var ret = new Array(response.length);
-            for (var i = 0; i < response.length; ++i) {
-                ret[i] = object.unwrapQObject(response[i]);
-            }
-            return ret;
-        }
-        if (!response
-            || !response["__QObject*__"]
-            || response.id === undefined) {
-            return response;
-        }
-
-        var objectId = response.id;
-        if (webChannel.objects[objectId])
-            return webChannel.objects[objectId];
-
-        if (!response.data) {
-            console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
-            return;
-        }
-
-        var qObject = new QObject( objectId, response.data, webChannel );
-        qObject.destroyed.connect(function() {
-            if (webChannel.objects[objectId] === qObject) {
-                delete webChannel.objects[objectId];
-                // reset the now deleted QObject to an empty {} object
-                // just assigning {} though would not have the desired effect, but the
-                // below also ensures all external references will see the empty map
-                // NOTE: this detour is necessary to workaround QTBUG-40021
-                var propertyNames = [];
-                for (var propertyName in qObject) {
-                    propertyNames.push(propertyName);
-                }
-                for (var idx in propertyNames) {
-                    delete qObject[propertyNames[idx]];
-                }
-            }
-        });
-        // here we are already initialized, and thus must directly unwrap the properties
-        qObject.unwrapProperties();
-        return qObject;
-    }
-
-    this.unwrapProperties = function()
-    {
-        for (var propertyIdx in object.__propertyCache__) {
-            object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
-        }
-    }
-
-    function addSignal(signalData, isPropertyNotifySignal)
-    {
-        var signalName = signalData[0];
-        var signalIndex = signalData[1];
-        object[signalName] = {
-            connect: function(callback) {
-                if (typeof(callback) !== "function") {
-                    console.error("Bad callback given to connect to signal " + signalName);
-                    return;
-                }
-
-                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
-                object.__objectSignals__[signalIndex].push(callback);
-
-                if (!isPropertyNotifySignal && signalName !== "destroyed") {
-                    // only required for "pure" signals, handled separately for properties in propertyUpdate
-                    // also note that we always get notified about the destroyed signal
-                    webChannel.exec({
-                        type: QWebChannelMessageTypes.connectToSignal,
-                        object: object.__id__,
-                        signal: signalIndex
-                    });
-                }
-            },
-            disconnect: function(callback) {
-                if (typeof(callback) !== "function") {
-                    console.error("Bad callback given to disconnect from signal " + signalName);
-                    return;
-                }
-                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
-                var idx = object.__objectSignals__[signalIndex].indexOf(callback);
-                if (idx === -1) {
-                    console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
-                    return;
-                }
-                object.__objectSignals__[signalIndex].splice(idx, 1);
-                if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
-                    // only required for "pure" signals, handled separately for properties in propertyUpdate
-                    webChannel.exec({
-                        type: QWebChannelMessageTypes.disconnectFromSignal,
-                        object: object.__id__,
-                        signal: signalIndex
-                    });
-                }
-            }
-        };
-    }
-
-    /**
-     * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
-     */
-    function invokeSignalCallbacks(signalName, signalArgs)
-    {
-        var connections = object.__objectSignals__[signalName];
-        if (connections) {
-            connections.forEach(function(callback) {
-                callback.apply(callback, signalArgs);
-            });
-        }
-    }
-
-    this.propertyUpdate = function(signals, propertyMap)
-    {
-        // update property cache
-        for (var propertyIndex in propertyMap) {
-            var propertyValue = propertyMap[propertyIndex];
-            object.__propertyCache__[propertyIndex] = propertyValue;
-        }
-
-        for (var signalName in signals) {
-            // Invoke all callbacks, as signalEmitted() does not. This ensures the
-            // property cache is updated before the callbacks are invoked.
-            invokeSignalCallbacks(signalName, signals[signalName]);
-        }
-    }
-
-    this.signalEmitted = function(signalName, signalArgs)
-    {
-        invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
-    }
-
-    function addMethod(methodData)
-    {
-        var methodName = methodData[0];
-        var methodIdx = methodData[1];
-        object[methodName] = function() {
-            var args = [];
-            var callback;
-            for (var i = 0; i < arguments.length; ++i) {
-                var argument = arguments[i];
-                if (typeof argument === "function")
-                    callback = argument;
-                else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
-                    args.push({
-                        "id": argument.__id__
-                    });
-                else
-                    args.push(argument);
-            }
-
-            webChannel.exec({
-                "type": QWebChannelMessageTypes.invokeMethod,
-                "object": object.__id__,
-                "method": methodIdx,
-                "args": args
-            }, function(response) {
-                if (response !== undefined) {
-                    var result = object.unwrapQObject(response);
-                    if (callback) {
-                        (callback)(result);
-                    }
-                }
-            });
-        };
-    }
-
-    function bindGetterSetter(propertyInfo)
-    {
-        var propertyIndex = propertyInfo[0];
-        var propertyName = propertyInfo[1];
-        var notifySignalData = propertyInfo[2];
-        // initialize property cache with current value
-        // NOTE: if this is an object, it is not directly unwrapped as it might
-        // reference other QObject that we do not know yet
-        object.__propertyCache__[propertyIndex] = propertyInfo[3];
-
-        if (notifySignalData) {
-            if (notifySignalData[0] === 1) {
-                // signal name is optimized away, reconstruct the actual name
-                notifySignalData[0] = propertyName + "Changed";
-            }
-            addSignal(notifySignalData, true);
-        }
-
-        Object.defineProperty(object, propertyName, {
-            configurable: true,
-            get: function () {
-                var propertyValue = object.__propertyCache__[propertyIndex];
-                if (propertyValue === undefined) {
-                    // This shouldn't happen
-                    console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
-                }
-
-                return propertyValue;
-            },
-            set: function(value) {
-                if (value === undefined) {
-                    console.warn("Property setter for " + propertyName + " called with undefined value!");
-                    return;
-                }
-                object.__propertyCache__[propertyIndex] = value;
-                var valueToSend = value;
-                if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
-                    valueToSend = { "id": valueToSend.__id__ };
-                webChannel.exec({
-                    "type": QWebChannelMessageTypes.setProperty,
-                    "object": object.__id__,
-                    "property": propertyIndex,
-                    "value": valueToSend
-                });
-            }
-        });
-
-    }
-
-    // ----------------------------------------------------------------------
-
-    data.methods.forEach(addMethod);
-
-    data.properties.forEach(bindGetterSetter);
-
-    data.signals.forEach(function(signal) { addSignal(signal, false); });
-
-    for (var name in data.enums) {
-        object[name] = data.enums[name];
-    }
-}
-
-//required for use with nodejs
-if (typeof module === 'object') {
-    module.exports = {
-        QWebChannel: QWebChannel
-    };
-}
-- 
GitLab