From 43c0823dec45faeb9ca7574f40bc838714ade28a Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Wed, 31 Oct 2018 13:42:09 -0400
Subject: [PATCH] message/call views: ui overhaul

This patch:
- implements QWebEngineView as the view for interactions
- reorganizes the main widget's layout in order to share views
  between in-call and out-of-call messaging views
- fixes behavioral bugs and crashes
- cannot be built with the mingw compiler due to lack of support for
  QWebEngine, and must be built natively with msvc and includes some
  build script modifications
- should be thought of as a new client

Change-Id: I59d8c68dc8384e85fb006f30d8313482c00d6c85
---
 RingWinClient.pro                             |   26 +-
 aboutdialog.cpp                               |    4 +-
 animatedoverlay.ui                            |   46 +
 animationhelpers.cpp                          |  130 +
 ...antmessagingwidget.h => animationhelpers.h |   75 +-
 build-client.bat                              |  117 +
 callwidget.cpp                                |  375 +--
 callwidget.h                                  |   24 +-
 callwidget.ui                                 | 2458 ++++++++---------
 conversationitemdelegate.cpp                  |   60 +-
 conversationsfilterwidget.cpp                 |   45 +-
 conversationsfilterwidget.h                   |   29 +-
 currentaccountcombobox.cpp                    |   29 +-
 currentaccountcombobox.h                      |   18 +-
 fetch-deps.bat                                |   27 +
 fetch-qrencodewin32.bat                       |   45 -
 fetch-winsparkle.bat                          |   45 -
 imdelegate.cpp                                |  191 --
 imdelegate.h                                  |   46 -
 instantmessagingwidget.cpp                    |  154 --
 instantmessagingwidget.ui                     |  124 -
 invitebuttonswidget.cpp                       |    3 +-
 invitebuttonswidget.h                         |    1 +
 linkify.js                                    | 1271 +++++++++
 lrcinstance.h                                 |    8 +
 main.cpp                                      |    7 +-
 mainwindow.cpp                                |    4 +-
 messagemodel.cpp                              |  123 -
 messagemodel.h                                |   61 -
 ...tionfilterbutton.cpp => messagewebpage.cpp |  107 +-
 ...ersationfilterbutton.h => messagewebpage.h |   80 +-
 messagewebview.cpp                            |  348 +++
 messagewebview.h                              |   81 +
 ressources.qrc                                |  113 +-
 ring-client-windows.sln                       |  114 +-
 ring-client-windows.vcxproj                   |  193 +-
 ring-client-windows.vcxproj.filters           |   76 +-
 ringthemeutils.h                              |    1 +
 settingskey.h                                 |    1 +
 smartlistmodel.cpp                            |    5 +-
 smartlistmodel.h                              |    6 +-
 smartlistview.cpp                             |   26 +-
 smartlistview.h                               |    3 -
 stylesheet.css                                |   42 +-
 touch_res.bat                                 |    1 +
 utils.cpp                                     |   13 +
 utils.h                                       |    6 +-
 version.h                                     |   52 +
 videoview.cpp                                 |    5 +-
 web/chatview.css                              |  921 ++++++
 web/chatview.html                             |   43 +
 web/chatview.js                               | 1504 ++++++++++
 web/linkify-html.js                           |  827 ++++++
 web/linkify-string.js                         |  118 +
 web/linkify.js                                | 1271 +++++++++
 web/qwebchannel.js                            |  427 +++
 webchathelpers.cpp                            |  141 +
 webchathelpers.h                              |   40 +
 58 files changed, 9391 insertions(+), 2720 deletions(-)
 create mode 100644 animatedoverlay.ui
 create mode 100644 animationhelpers.cpp
 rename instantmessagingwidget.h => animationhelpers.h (51%)
 create mode 100644 build-client.bat
 create mode 100644 fetch-deps.bat
 delete mode 100644 fetch-qrencodewin32.bat
 delete mode 100644 fetch-winsparkle.bat
 delete mode 100644 imdelegate.cpp
 delete mode 100644 imdelegate.h
 delete mode 100644 instantmessagingwidget.cpp
 delete mode 100644 instantmessagingwidget.ui
 create mode 100644 linkify.js
 delete mode 100644 messagemodel.cpp
 delete mode 100644 messagemodel.h
 rename conversationfilterbutton.cpp => messagewebpage.cpp (52%)
 rename conversationfilterbutton.h => messagewebpage.h (68%)
 create mode 100644 messagewebview.cpp
 create mode 100644 messagewebview.h
 create mode 100644 touch_res.bat
 create mode 100644 version.h
 create mode 100644 web/chatview.css
 create mode 100644 web/chatview.html
 create mode 100644 web/chatview.js
 create mode 100644 web/linkify-html.js
 create mode 100644 web/linkify-string.js
 create mode 100644 web/linkify.js
 create mode 100644 web/qwebchannel.js
 create mode 100644 webchathelpers.cpp
 create mode 100644 webchathelpers.h

diff --git a/RingWinClient.pro b/RingWinClient.pro
index 3174caf..735b90d 100644
--- a/RingWinClient.pro
+++ b/RingWinClient.pro
@@ -53,11 +53,9 @@ SOURCES += main.cpp\
     windowscontactbackend.cpp \
     selectareadialog.cpp \
     accountserializationadapter.cpp \
-    instantmessagingwidget.cpp \
     accountstatedelegate.cpp \
     videoview.cpp \
     videooverlay.cpp \
-    imdelegate.cpp \
     contactpicker.cpp \
     globalsystemtray.cpp \
     conversationitemdelegate.cpp \
@@ -79,11 +77,14 @@ SOURCES += main.cpp\
     smartlistview.cpp \
     accountitemdelegate.cpp \
     accountlistmodel.cpp \
-    messagemodel.cpp \
     invitebuttonswidget.cpp \
     wizardwidget.cpp \
     currentaccountcombobox.cpp \
-    conversationfilterbutton.cpp
+    conversationfilterbutton.cpp \
+    messagewebpage.cpp \
+    messagewebview.cpp \
+    webchathelpers.cpp \
+    animationhelpers.cpp
 
 HEADERS  += mainwindow.h \
     callwidget.h \
@@ -97,11 +98,9 @@ HEADERS  += mainwindow.h \
     windowscontactbackend.h \
     selectareadialog.h \
     accountserializationadapter.h \
-    instantmessagingwidget.h \
     accountstatedelegate.h \
     videoview.h \
     videooverlay.h \
-    imdelegate.h \
     contactpicker.h \
     settingskey.h \
     globalsystemtray.h \
@@ -126,11 +125,14 @@ HEADERS  += mainwindow.h \
     smartlistview.h \
     accountitemdelegate.h \
     accountlistmodel.h \
-    messagemodel.h \
     invitebuttonswidget.h \
     wizardwidget.h \
     currentaccountcombobox.h \
-    conversationfilterbutton.h
+    conversationfilterbutton.h \
+    messagewebpage.h \
+    messagewebview.h \
+    webchathelpers.h \
+    animationhelpers.h
 
 
 contains(DEFINES, URI_PROTOCOL) {
@@ -144,7 +146,6 @@ FORMS    += mainwindow.ui \
     accountdetails.ui \
     aboutdialog.ui \
     wizarddialog.ui \
-    instantmessagingwidget.ui \
     videoview.ui \
     videooverlay.ui \
     contactpicker.ui \
@@ -158,7 +159,8 @@ FORMS    += mainwindow.ui \
     bannedcontactswidget.ui \
     photoboothwidget.ui \
     invitebuttonswidget.ui \
-    wizardwidget.ui
+    wizardwidget.ui \
+    animatedoverlay.ui
 
 win32: LIBS += -lole32 -luuid -lshlwapi -lgdi32
 LIBS += -lqrencode
@@ -273,7 +275,9 @@ win32 {
     QTRUNTIME.files = $$RUNTIMEDIR/Qt5Core.dll $$RUNTIMEDIR/Qt5Widgets.dll \
                             $$RUNTIMEDIR/Qt5Gui.dll $$RUNTIMEDIR/Qt5Svg.dll \
                             $$RUNTIMEDIR/Qt5Xml.dll $$RUNTIMEDIR/Qt5WinExtras.dll \
-                            $$RUNTIMEDIR/Qt5Network.dll $$RUNTIMEDIR/Qt5Sql.dll
+                            $$RUNTIMEDIR/Qt5Network.dll $$RUNTIMEDIR/Qt5Sql.dll \
+                            $$RUNTIMEDIR/Qt5WebEngineWidgets.dll $$RUNTIMEDIR/Qt5WebChannel.dll
+
     QTRUNTIME.path = $$OUT_PWD/release
 
     QTDEPSRUNTIME.files = $$RUNTIMEDIR/zlib1.dll \
diff --git a/aboutdialog.cpp b/aboutdialog.cpp
index 9172c1d..ec5d0a7 100644
--- a/aboutdialog.cpp
+++ b/aboutdialog.cpp
@@ -19,6 +19,8 @@
 #include "aboutdialog.h"
 #include "ui_aboutdialog.h"
 
+#include "version.h"
+
 AboutDialog::AboutDialog(QWidget *parent) :
     QDialog(parent),
     ui(new Ui::AboutDialog)
@@ -27,7 +29,7 @@ AboutDialog::AboutDialog(QWidget *parent) :
 
     this->setFixedSize(this->width(),this->height());
     ui->creditsWidget->hide();
-    ui->gitVersionLabel->setText(QString("%1: %2").arg(tr("version"), NIGHTLY_VERSION));
+    ui->gitVersionLabel->setText(QString("%1: %2").arg(tr("version"), QString(VERSION_STRING)));
 
     ui->creditsBrowser->setHtml("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">"
                                 "<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">"
diff --git a/animatedoverlay.ui b/animatedoverlay.ui
new file mode 100644
index 0000000..4aff548
--- /dev/null
+++ b/animatedoverlay.ui
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AnimatedOverlay</class>
+ <widget class="QWidget" name="AnimatedOverlay">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <item row="0" column="0">
+    <widget class="QLabel" name="backgroundLabel">
+     <property name="styleSheet">
+      <string notr="true"/>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/animationhelpers.cpp b/animationhelpers.cpp
new file mode 100644
index 0000000..c2abd8d
--- /dev/null
+++ b/animationhelpers.cpp
@@ -0,0 +1,130 @@
+/***************************************************************************
+ * Copyright (C) 2018 by Savoir-faire Linux                                *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#include "animationhelpers.h"
+
+#include "ui_animatedoverlay.h"
+
+#include <QTimer>
+#include <QtMath>
+
+OpacityAnimation::OpacityAnimation(QWidget* target, QObject* parent)
+    : QObject(parent),
+    target_(target),
+    timer_(nullptr),
+    frameTime_((1.0 / 24.0) * 1000),
+    startValue_(0.0),
+    endValue_(1.0),
+    t_(0),
+    value_(0),
+    duration_(1000)
+{
+    timer_ = new QTimer(this);
+    connect(timer_, SIGNAL(timeout()), this, SLOT(updateAnimation()));
+
+    effect_ = new QGraphicsOpacityEffect(this);
+    effect_->setOpacity(startValue_);
+
+    target_->setGraphicsEffect(effect_);
+    target_->setAutoFillBackground(true);
+}
+
+OpacityAnimation::~OpacityAnimation()
+{
+}
+
+void
+OpacityAnimation::setFPS(const int& fps)
+{
+    frameTime_ = (1.0 / static_cast<double>(fps)) * 1000;
+}
+
+void
+OpacityAnimation::setFrameTime(const int& milliseconds)
+{
+    frameTime_ = milliseconds;
+}
+
+void
+OpacityAnimation::setDuration(const int& milliseconds)
+{
+    duration_ = milliseconds;
+}
+
+void
+OpacityAnimation::setStartValue(const double& value)
+{
+    startValue_ = value;
+    effect_->setOpacity(startValue_);
+}
+
+void
+OpacityAnimation::setEndValue(const double& value)
+{
+    endValue_ = value;
+}
+
+void
+OpacityAnimation::start()
+{
+    timer_->start(frameTime_);
+}
+
+void
+OpacityAnimation::stop()
+{
+    timer_->stop();
+}
+
+void
+OpacityAnimation::updateAnimation()
+{
+    double d = (startValue_ + endValue_) * 0.5;
+    double a = abs(startValue_ - endValue_) * 0.5;
+
+    t_ += frameTime_;
+    value_ = a * sin(2 * M_PI * t_ * duration_ * .000001) + d;
+    effect_->setOpacity(value_);
+    target_->update();
+}
+
+AnimatedOverlay::AnimatedOverlay(QColor color, QWidget* parent) :
+    QWidget(parent),
+    ui(new Ui::AnimatedOverlay)
+{
+    ui->setupUi(this);
+    ui->backgroundLabel->setAutoFillBackground(true);
+    auto values = QString("%1,%2,%3,255")
+        .arg(color.red())
+        .arg(color.green())
+        .arg(color.blue());
+    ui->backgroundLabel->setStyleSheet("background-color: rgba(" + values + ");");
+
+    oa_ = new OpacityAnimation(this, this);
+    oa_->setFPS(16);
+    oa_->setDuration(1000);
+    oa_->setStartValue(0.0);
+    oa_->setEndValue(0.25);
+    oa_->start();
+}
+
+AnimatedOverlay::~AnimatedOverlay()
+{
+    disconnect(this);
+    delete ui;
+}
diff --git a/instantmessagingwidget.h b/animationhelpers.h
similarity index 51%
rename from instantmessagingwidget.h
rename to animationhelpers.h
index ed4a79a..e9cee03 100644
--- a/instantmessagingwidget.h
+++ b/animationhelpers.h
@@ -1,6 +1,6 @@
 /***************************************************************************
- * Copyright (C) 2015-2017 by Savoir-faire Linux                                *
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
+ * Copyright (C) 2018 by Savoir-faire Linux                                *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
  *                                                                         *
  * This program is free software; you can redistribute it and/or modify    *
  * it under the terms of the GNU General Public License as published by    *
@@ -19,49 +19,56 @@
 #pragma once
 
 #include <QWidget>
-#include <QKeyEvent>
-#include <QSettings>
+#include <QPropertyAnimation>
+#include <QGraphicsOpacityEffect>
+#include <QMovie>
 
-#include "call.h"
-#include "media/media.h"
-
-#include "imdelegate.h"
-#include "messagemodel.h"
-
-namespace Ui {
-class InstantMessagingWidget;
-}
-
-class InstantMessagingWidget final : public QWidget
+class OpacityAnimation : public QObject
 {
     Q_OBJECT
-
 public:
-    explicit InstantMessagingWidget(QWidget *parent = 0);
-    ~InstantMessagingWidget();
-    void setupCallMessaging(const std::string& callId,
-                            MessageModel *messageModel);
+    explicit OpacityAnimation(QWidget* target, QObject* parent = nullptr);
+    ~OpacityAnimation();
 
-protected:
-    virtual void keyPressEvent(QKeyEvent *event) override;
-    virtual void showEvent(QShowEvent * event) override;
+    void setFPS(const int& fps);
+    void setFrameTime(const int& milliseconds);
+    void setDuration(const int& milliseconds);
+    void setStartValue(const double& value);
+    void setEndValue(const double& value);
 
-//UI SLOTS
-private slots:
-    void on_sendButton_clicked();
+    void start();
+    void stop();
 
 private slots:
-    void onIncomingMessage(const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction);
+    void updateAnimation();
 
 private:
-    Ui::InstantMessagingWidget *ui;
-    ImDelegate* imDelegate_;
-    std::unique_ptr<MessageModel> messageModel_;
-    QSettings settings_;
-    QMetaObject::Connection newInteractionConnection_;
+    QGraphicsOpacityEffect* effect_;
+    double value_;
 
-    void copyToClipboard();
-    void updateConversationView(const std::string& convUid);
+    QWidget* target_;
+    QTimer* timer_;
+    int frameTime_;
+    double t_;
+    int duration_;
 
+    double startValue_;
+    double endValue_;
 };
 
+namespace Ui {
+class AnimatedOverlay;
+}
+
+class AnimatedOverlay : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit AnimatedOverlay(QColor color, QWidget* parent = 0);
+    ~AnimatedOverlay();
+
+private:
+    Ui::AnimatedOverlay* ui;
+
+    OpacityAnimation* oa_;
+};
\ No newline at end of file
diff --git a/build-client.bat b/build-client.bat
new file mode 100644
index 0000000..874ba2b
--- /dev/null
+++ b/build-client.bat
@@ -0,0 +1,117 @@
+:: Ring - native Windows client project build script
+
+@echo off
+setlocal
+
+if "%1" == "/?" goto Usage
+if "%~1" == "" goto Usage
+
+set doCompile=N
+set doBuild=N
+
+set SCRIPTNAME=%~nx0
+
+if "%1"=="compile" (
+    set doCompile=Y
+) else if "%1"=="build" (
+    set doBuild=Y
+) else (
+    goto Usage
+)
+
+set arch=N
+
+shift
+:ParseArgs
+if "%1" == "" goto FinishedArgs
+if /I "%1"=="x86" (
+    set arch=x86
+) else if /I "%1"=="x64" (
+    set arch=x64
+) else (
+    goto Usage
+)
+shift
+goto ParseArgs
+
+:FinishedArgs
+if "%arch%"=="x86" (
+    set MSBUILD_ARGS=/nologo /p:useenv=true /p:Platform=Win32 /maxcpucount:%NUMBER_OF_PROCESSORS%
+) else if "%arch%"=="x64" (
+    set MSBUILD_ARGS=/nologo /p:useenv=true /p:Platform=x64 /maxcpucount:%NUMBER_OF_PROCESSORS%
+)
+
+@setlocal
+
+set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer"
+if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer"
+
+pushd %VSInstallerFolder%
+for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
+  set VSLATESTDIR=%%i
+)
+popd
+
+echo VS Installation folder: %VSLATESTDIR%
+
+if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" (
+    echo:
+    echo VSInstallDir not found or not installed correctly.
+    goto cleanup
+)
+
+if %PROCESSOR_ARCHITECTURE%==x86 (
+    set Comp_x86=x86 10.0.15063.0
+    set Comp_x64=x86_amd64 10.0.15063.0
+) else (
+    set Comp_x86=amd64_x86 10.0.15063.0
+    set Comp_x64=amd64 10.0.15063.0
+)
+
+set path=%path:"=%
+if "%arch%"=="x86" (
+    call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x86%
+) else if "%arch%"=="x64" (
+    call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64%
+)
+
+if "%arch%" neq "N" (
+    if "%doCompile%" neq "N" (
+        goto compileClient
+    ) else if "%doBuild%" neq "N" (
+        goto buildClient
+    )
+    goto :eof
+)
+goto Usage
+
+:compileClient
+msbuild ring-client-windows.vcxproj /verbosity:normal /p:Configuration=ReleaseCompile %MSBUILD_ARGS%
+goto cleanup
+
+:buildClient
+msbuild ring-client-windows.vcxproj /verbosity:normal /p:Configuration=Release %MSBUILD_ARGS%
+goto cleanup
+
+@endlocal
+
+:Usage
+echo:
+echo The correct usage is:
+echo:
+echo     %0 [action] [architecture]
+echo:
+echo where
+echo:
+echo [action]           is: compile  ^| build
+echo [architecture]     is: x86      ^| x64
+echo:
+echo For example:
+echo     %0 compile x86    - compile only x86 (for CI)
+echo     %0 build x64      - build x64 client
+echo:
+goto :eof
+
+:cleanup
+endlocal
+exit /B %ERRORLEVEL%
\ No newline at end of file
diff --git a/callwidget.cpp b/callwidget.cpp
index 71b29a7..d5679f2 100644
--- a/callwidget.cpp
+++ b/callwidget.cpp
@@ -27,6 +27,7 @@
 #include <QClipboard>
 #include <QDesktopServices>
 #include <QComboBox>
+#include <QWebEngineScript>
 
 #include <memory>
 
@@ -48,17 +49,16 @@
 #include "contactpicker.h"
 #include "globalsystemtray.h"
 #include "conversationitemdelegate.h"
-#include "imdelegate.h"
 #include "pixbufmanipulator.h"
 #include "settingskey.h"
 #include "lrcinstance.h"
-#include "messagemodel.h"
+#include "animationhelpers.h"
+#include "ringthemeutils.h"
 
 CallWidget::CallWidget(QWidget* parent) :
     NavWidget(parent),
     ui(new Ui::CallWidget),
-    menu_(new QMenu()),
-    imDelegate_(new ImDelegate())
+    menu_(new QMenu())
 {
     ui->setupUi(this);
 
@@ -77,11 +77,12 @@ CallWidget::CallWidget(QWidget* parent) :
     // this line is not welcome here, and must be removed
     ProfileModel::instance().addCollection<LocalProfileCollection>(LoadOptions::FORCE_ENABLED);
 
+    QSettings settings;
+
     // select last used account if stored in registry
     auto accountList = LRCInstance::accountModel().getAccountList();
     if (!accountList.empty()) {
         std::string accountIdToStartWith;
-        QSettings settings;
         if (settings.contains(SettingsKey::selectedAccount)) {
             accountIdToStartWith = settings
                 .value(SettingsKey::selectedAccount, true)
@@ -102,11 +103,19 @@ CallWidget::CallWidget(QWidget* parent) :
         }
     }
 
+    if (settings.contains(SettingsKey::mainSplitterState)) {
+        auto splitterStates = settings.value(SettingsKey::mainSplitterState).toByteArray();
+        ui->mainActivitySplitter->restoreState(splitterStates);
+    }
+
+    ui->mainActivitySplitter->setCollapsible(0, false);
+    ui->splitter->setCollapsible(0, false);
+    ui->splitter->setCollapsible(1, false);
+
     //disable dropdown shadow on combobox
     ui->currentAccountComboBox->view()->window()->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
 
     // conversation list
-    conversationItemDelegate_ = new ConversationItemDelegate();
     ui->smartList->setContextMenuPolicy(Qt::CustomContextMenu);
 
     // setup searchingfor mini spinner
@@ -114,29 +123,31 @@ CallWidget::CallWidget(QWidget* parent) :
     ui->spinnerLabel->setMovie(miniSpinner_);
     ui->spinnerLabel->hide();
 
-    ui->listMessageView->verticalScrollBar()->setEnabled(true);
-    ui->listMessageView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
-    ui->listMessageView->verticalScrollBar()->setStyleSheet("QScrollBar:vertical { width: 0px; }");
-
-    setupOutOfCallIM();
-
     // connections
     connect(ui->currentAccountComboBox, &CurrentAccountComboBox::settingsButtonClicked,
             this, &CallWidget::settingsButtonClicked);
 
-    connect(ui->videoWidget, SIGNAL(setChatVisibility(bool)),
-            ui->instantMessagingWidget, SLOT(setVisible(bool)));
+    connect(ui->videoWidget, &VideoView::setChatVisibility,
+        [this](bool visible) {
+            ui->callStackWidget->hide();
+        });
+
+    connect(ui->mainActivitySplitter, &QSplitter::splitterMoved,
+        [this](int pos, int index) {
+            QSettings settings;
+            settings.setValue(SettingsKey::mainSplitterState, ui->mainActivitySplitter->saveState());
+        });
 
     connect(ui->videoWidget, &VideoView::videoSettingsClicked,
             this, &CallWidget::settingsButtonClicked);
 
-    connect(ui->buttonConversations, &QPushButton::clicked,
+    connect(ui->btnConversations, &QPushButton::clicked,
             this, &CallWidget::conversationsButtonClicked);
 
-    connect(ui->buttonInvites, &QPushButton::clicked,
+    connect(ui->btnInvites, &QPushButton::clicked,
             this, &CallWidget::invitationsButtonClicked);
 
-    connect(ui->smartList, &QListView::customContextMenuRequested,
+    connect(ui->smartList, &QTreeView::customContextMenuRequested,
             this, &CallWidget::slotCustomContextMenuRequested);
 
     connect(ui->smartList, &SmartListView::btnAcceptInviteClicked,
@@ -169,48 +180,25 @@ CallWidget::CallWidget(QWidget* parent) :
     connect(ui->btnVideoCall, &QPushButton::clicked,
             this, &CallWidget::on_sendContactRequestButton_clicked);
 
-    // connect conversation filter buttons to act as radio buttons
-    connect(ui->buttonInvites, &ConversationFilterButton::clicked,
-            ui->buttonConversations, &ConversationFilterButton::setUnselected);
-
-    connect(ui->buttonConversations, &ConversationFilterButton::clicked,
-            ui->buttonInvites, &ConversationFilterButton::setUnselected);
-
     connect(ui->currentAccountComboBox, QOverload<int>::of(&CurrentAccountComboBox::currentIndexChanged),
         [this] {
-            ui->buttonConversations->setSelected();
-            ui->buttonInvites->setUnselected();
-    });
+            ui->btnConversations->setChecked(true);
+            ui->btnInvites->setChecked(false);
+        });
+
 
     // set first view to welcome view
     ui->stackedWidget->setCurrentWidget(ui->welcomePage);
-    ui->buttonConversations->setSelected();
+    ui->btnConversations->setChecked(true);
+
+    // chat view
+    ui->messageView->buildView();
 }
 
 CallWidget::~CallWidget()
 {
     delete ui;
     delete menu_;
-    delete imDelegate_;
-    delete conversationItemDelegate_;
-}
-
-void
-CallWidget::setupOutOfCallIM()
-{
-    ui->listMessageView->setItemDelegate(imDelegate_);
-    ui->listMessageView->setContextMenuPolicy(Qt::ActionsContextMenu);
-
-    auto copyAction = new QAction(tr("Copy"), this);
-    ui->listMessageView->addAction(copyAction);
-
-    connect(copyAction, &QAction::triggered, [=]() {
-        auto idx = ui->listMessageView->currentIndex();
-        if (idx.isValid()) {
-            auto text = ui->listMessageView->model()->data(idx);
-            QApplication::clipboard()->setText(text.value<QString>());
-        }
-    });
 }
 
 void
@@ -220,11 +208,24 @@ CallWidget::onIncomingMessage(const std::string& convUid,
 {
     Q_UNUSED(interactionId);
     if (!QApplication::focusWidget()) {
+        auto convModel = LRCInstance::getCurrentConversationModel();
+        auto conversation = Utils::getConversationFromUid(convUid, *convModel);
+        auto bestName = Utils::bestNameForConversation(*conversation, *convModel);
         Utils::showSystemNotification(this,
             QString(tr("Message incoming from %1"))
-            .arg(QString::fromStdString(interaction.body)));
+            .arg(QString::fromStdString(bestName)));
+    }
+    if (convUid != selectedConvUid()) {
+        return;
     }
-    updateConversationView(convUid);
+
+    auto convModel = LRCInstance::getCurrentConversationModel();
+    convModel->clearUnreadInteractions(convUid);
+    auto currentConversation = Utils::getConversationFromUid(convUid, *convModel);
+    if (currentConversation == convModel->allFilteredConversations().end()) {
+        return;
+    }
+    ui->messageView->printNewInteraction(*convModel, interactionId, interaction);
     ui->conversationsFilterWidget->update();
 }
 
@@ -306,9 +307,9 @@ CallWidget::setupSmartListContextMenu(const QPoint& pos)
                 );
             });
     }
-    smartListModel_->isContextMenuOpen_ = true;
+    smartListModel_->isContextMenuOpen = true;
     menu.exec(globalPos);
-    smartListModel_->isContextMenuOpen_ = false;
+    smartListModel_->isContextMenuOpen = false;
 }
 
 void
@@ -386,18 +387,9 @@ CallWidget::on_cancelButton_clicked()
 void
 CallWidget::showConversationView()
 {
-    ui->stackedWidget->setCurrentWidget(ui->messagingPage);
-    ui->imMessageEdit->clear();
-    ui->imMessageEdit->setFocus();
-    disconnect(imClickedConnection_);
-    imClickedConnection_ = connect(ui->listMessageView, &QListView::clicked, [this](const QModelIndex& index) {
-        auto urlList = index.data(static_cast<int>(media::TextRecording::Role::LinkList)).value<QList<QUrl>>();
-        if (urlList.size() == 1) {
-            QDesktopServices::openUrl(urlList.at(0));
-        } else if (urlList.size()) {
-            //TODO Handle multiple url in one message
-        }
-    });
+    ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget);
+    ui->messageView->setFocus();
+
 }
 
 void
@@ -475,21 +467,6 @@ CallWidget::smartListSelectionChanged(const QItemSelection  &selected, const QIt
     selectConversation(selectedIndex);
 }
 
-void
-CallWidget::contactReqListCurrentChanged(const QModelIndex &currentIdx, const QModelIndex &previousIdx)
-{
-    Q_UNUSED(previousIdx)
-
-    if (currentIdx.isValid()) {
-        ui->contactRequestWidget->setCurrentContactRequest(currentIdx);
-        ui->stackedWidget->setCurrentWidget(ui->contactRequestPage);
-    } else {
-        ui->contactRequestWidget->setCurrentContactRequest(QModelIndex());
-        if (ui->stackedWidget->currentWidget() == ui->contactRequestPage)
-            Utils::setStackWidget(ui->stackedWidget, ui->welcomePage);
-    }
-}
-
 void
 CallWidget::placeCall()
 {
@@ -498,21 +475,27 @@ CallWidget::placeCall()
     Call* c = CallModel::instance().dialingCall(PhoneDirectoryModel::instance().getNumber(ui->ringContactLineEdit->text()));
     c->performAction(Call::Action::ACCEPT);
     ui->ringContactLineEdit->clear();
+    auto photoRect = ui->callingPhoto->frameGeometry();
     ui->callingPhoto->setPixmap(
                 QPixmap::fromImage(
                     GlobalInstances::pixmapManipulator()
-                    .callPhoto(c, QSize(130,130)).value<QImage>()));
+                    .callPhoto(c, QSize(photoRect.width(), photoRect.height()))
+                    .value<QImage>()));
 }
 
 void
 CallWidget::conversationsButtonClicked()
 {
+    ui->btnConversations->setChecked(true);
+    ui->btnInvites->setChecked(false);
     setConversationFilter(lrc::api::profile::Type::RING);
 }
 
 void
 CallWidget::invitationsButtonClicked()
 {
+    ui->btnConversations->setChecked(false);
+    ui->btnInvites->setChecked(true);
     setConversationFilter(lrc::api::profile::Type::PENDING);
 }
 
@@ -535,8 +518,8 @@ CallWidget::on_ringContactLineEdit_returnPressed()
     // select current temporary item and show conversation
     auto convModel = LRCInstance::getCurrentConversationModel();
     auto conversations = convModel->allFilteredConversations();
-    auto contactIsValid = Utils::isContactValid(conversations.at(0).participants.at(0), *convModel);
-    if (!conversations.empty() && contactIsValid) {
+    if (!conversations.empty() &&
+        Utils::isContactValid(conversations.at(0).participants.at(0), *convModel)) {
         selectConversation(smartListModel_->index(0));
     }
 }
@@ -575,65 +558,112 @@ void CallWidget::slotShowCallView(const std::string& accountId,
 {
     Q_UNUSED(accountId);
     Q_UNUSED(convInfo);
-    qDebug() << "BehaviorController::showCallView";
-    ui->stackedWidget->setCurrentWidget(ui->videoPage);
+    qDebug() << "slotShowCallView";
+    ui->callStackWidget->show();
+    ui->callStackWidget->setCurrentWidget(ui->videoPage);
     hideMiniSpinner();
 }
 
 void CallWidget::slotShowIncomingCallView(const std::string& accountId,
-                                          const lrc::api::conversation::Info& convInfo) {
+                                          const lrc::api::conversation::Info& convInfo)
+{
     Q_UNUSED(accountId);
-    qDebug() << "BehaviorController::showIncomingCallView";
+    qDebug() << "slotShowIncomingCallView";
+
     auto callModel = LRCInstance::getCurrentCallModel();
+
+    if (!callModel->hasCall(convInfo.callId)) {
+        return;
+    }
+
+    auto convModel = LRCInstance::getCurrentConversationModel();
+    ui->callerPhoto->setPixmap(QPixmap::fromImage(imageForConv(convInfo.uid)));
+    auto bestName = QString::fromStdString(Utils::bestNameForConversation(convInfo, *convModel));
+    auto bestId = QString::fromStdString(Utils::bestIdForConversation(convInfo, *convModel));
+    auto finalBestId = (bestName != bestId) ? bestId : "";
+
     auto call = callModel->getCall(convInfo.callId);
+    auto isCallSelected = selectedConvUid() == convInfo.uid;
+
     if (call.isOutgoing) {
-        miniSpinner_->start();
-        ui->spinnerLabel->show();
-        ui->stackedWidget->setCurrentWidget(ui->outboundCallPage);
-    }
-    else {
-        selectSmartlistItem(convInfo.uid);
-        auto selectedAccountId = LRCInstance::getCurrentAccountInfo().id;
-        auto accountProperties = LRCInstance::accountModel().getAccountConfig(selectedAccountId);
-        if (!accountProperties.autoAnswer) {
-            ui->stackedWidget->setCurrentWidget(ui->callInvitePage);
-        }
-        else {
-            ui->stackedWidget->setCurrentWidget(ui->videoPage);
+        if (isCallSelected) {
+            miniSpinner_->start();
+            ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget);
+            ui->spinnerLabel->show();
+            ui->callStackWidget->setCurrentWidget(ui->outgoingCallPage);
+            ui->callStackWidget->show();
         }
+    } else {
         if (!QApplication::focusWidget()) {
-            auto formattedName = Utils::bestNameForConversation(convInfo, *LRCInstance::getCurrentConversationModel());
+            auto formattedName = Utils::bestNameForConversation(convInfo, *convModel);
             Utils::showSystemNotification(this,
                 QString(tr("Call incoming from %1")).arg(QString::fromStdString(formattedName)));
         }
+        selectSmartlistItem(convInfo.uid);
+        auto selectedAccountId = LRCInstance::getCurrentAccountInfo().id;
+        auto accountProperties = LRCInstance::accountModel().getAccountConfig(selectedAccountId);
+        if (accountProperties.autoAnswer) {
+            ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget);
+            ui->callStackWidget->setCurrentWidget(ui->videoPage);
+        } else if (isCallSelected) {
+            ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget);
+            ui->callStackWidget->setCurrentWidget(ui->incomingCallPage);
+            ui->callStackWidget->show();
+        }
     }
 
-    if (!callModel->hasCall(convInfo.callId)) {
-        return;
+    // flashing index widget during call
+    for (int row = 0; row < smartListModel_->rowCount(); row++) {
+        QModelIndex index = smartListModel_->index(row);
+        auto indexUid = index.data(SmartListModel::Role::UID).value<QString>().toStdString();
+        if (indexUid == convInfo.uid) {
+            auto widget = ui->smartList->indexWidget(index);
+            if (!widget) {
+                auto blinkingWidget = new AnimatedOverlay(RingTheme::urgentOrange_);
+                ui->smartList->setIndexWidget(index, blinkingWidget);
+                blinkingWidget->show();
+            } else {
+                widget->show();
+            }
+        }
     }
 
     ui->videoWidget->pushRenderer(convInfo.callId);
 
-    ui->instantMessagingWidget->setupCallMessaging(convInfo.callId, messageModel_.get());
+    QFontMetrics primaryCallLabelFontMetrics(ui->callingBestNameLabel->font());
+    QFontMetrics sencondaryCallLabelFontMetrics(ui->callingBestIdLabel->font());
 
-    disconnect(selectedCallChanged_);
-    selectedCallChanged_ = connect(
-        callModel,
-        &lrc::api::NewCallModel::callStatusChanged,
-        [this, callModel](const std::string& callUid) {
-            auto call = callModel->getCall(callUid);
-            qDebug() << "NewCallModel::callStatusChanged: " << static_cast<int>(call.status);
-        }
-    );
+    QString elidedLabel = primaryCallLabelFontMetrics.elidedText(bestName, Qt::ElideRight, ui->callerBestNameLabel->width());
+    ui->callerBestNameLabel->setText(elidedLabel);
+
+    elidedLabel = primaryCallLabelFontMetrics.elidedText(bestName, Qt::ElideRight, ui->callingBestNameLabel->width());
+    ui->callingBestNameLabel->setText(elidedLabel);
 
-    ui->callerPhoto->setPixmap(QPixmap::fromImage(imageForConv(selectedConvUid())));
+    elidedLabel = sencondaryCallLabelFontMetrics.elidedText(finalBestId, Qt::ElideRight, ui->callingBestIdLabel->width());
+    ui->callingBestIdLabel->setText(elidedLabel);
+
+    ui->smartList->update();
 }
 
 void CallWidget::slotShowChatView(const std::string& accountId,
-                      const lrc::api::conversation::Info& convInfo) {
+                      const lrc::api::conversation::Info& convInfo)
+{
     Q_UNUSED(accountId);
     Q_UNUSED(convInfo);
-    qDebug() << "BehaviorController::showChatView";
+
+    for (int row = 0; row < smartListModel_->rowCount(); row++) {
+        QModelIndex index = smartListModel_->index(row);
+        auto indexUid = index.data(SmartListModel::Role::UID).value<QString>().toStdString();
+        if (indexUid == convInfo.uid) {
+            if (auto widget = ui->smartList->indexWidget(index)) {
+                widget->hide();
+                widget->deleteLater();
+                ui->smartList->setIndexWidget(index, nullptr);
+            }
+        }
+    }
+
+    ui->callStackWidget->hide();
     showConversationView();
 }
 
@@ -681,6 +711,7 @@ void CallWidget::updateConversationsFilterWidget()
         LRCInstance::getCurrentConversationModel()->setFilter(currentTypeFilter_);
     }
     ui->conversationsFilterWidget->setVisible(invites);
+    ui->conversationsFilterWidget->update();
 }
 
 void CallWidget::setConversationFilter(const QString & filter)
@@ -705,16 +736,6 @@ CallWidget::showIMOutOfCall(const QModelIndex& nodeIdx)
     }
     catch (...) {}
 
-    if (!isRINGAccount){
-        ui->imMessageEdit->setPlaceholderText("No messaging possible out of call (SIP) ");
-        ui->imMessageEdit->setEnabled(false);
-        ui->sendIMButton->hide();
-    } else {
-        ui->imMessageEdit->setPlaceholderText("Type your message here");
-        ui->imMessageEdit->setEnabled(true);
-        ui->sendIMButton->show();
-    }
-
     ui->imNameLabel->setText(QString(tr("%1", "%1 is the contact username"))
                                     .arg(displayName));
 
@@ -729,44 +750,34 @@ CallWidget::showIMOutOfCall(const QModelIndex& nodeIdx)
     bool shouldShowSendContactRequestBtn = !isContact && isRINGAccount;
     ui->sendContactRequestButton->setVisible(shouldShowSendContactRequestBtn);
 
-    showConversationView();
-
+    auto convModel = LRCInstance::getCurrentConversationModel();
     auto currentConversation = Utils::getConversationFromUid(selectedConvUid(),
-                                                             *LRCInstance::getCurrentConversationModel());
-    messageModel_.reset(new MessageModel(*currentConversation, accountInfo, this->parent()));
-    ui->listMessageView->setModel(messageModel_.get());
-    ui->listMessageView->scrollToBottom();
-}
+                                                             *convModel);
+    ui->messageView->clear();
+    ui->messageView->printHistory(*convModel, currentConversation->interactions);
 
-void
-CallWidget::on_sendIMButton_clicked()
-{
-    auto msg = ui->imMessageEdit->text();
-    if (msg.trimmed().isEmpty()) return;
-    ui->imMessageEdit->clear();
-    try {
-        LRCInstance::getCurrentConversationModel()->sendMessage(selectedConvUid(), msg.toStdString());
-    } catch (...) {
-        qDebug() << "exception when sending message";
-    }
-}
-
-void
-CallWidget::on_imMessageEdit_returnPressed()
-{
-    on_sendIMButton_clicked();
-}
-
-void
-CallWidget::slotAccountMessageReceived(const QMap<QString,QString> message,
-                                       ContactMethod* cm,
-                                       media::Media::Direction dir)
-{
-    Q_UNUSED(message)
-    Q_UNUSED(dir)
+    showConversationView();
 
-    ui->listMessageView->scrollToBottom();
-    cm->textRecording()->setAllRead();
+    // Contact Avatars
+    auto accInfo = &LRCInstance::getCurrentAccountInfo();
+    auto contactUri = currentConversation->participants.front();
+    try {
+        auto& contact = accInfo->contactModel->getContact(contactUri);
+        if (!contact.profileInfo.avatar.empty()) {
+            ui->messageView->setSenderImage(
+                accInfo->contactModel->getContactProfileId(contactUri),
+                contact.profileInfo.avatar);
+        } else {
+            auto avatar = Utils::conversationPhoto(selectedConvUid(), *accInfo);
+            QByteArray ba;
+            QBuffer bu(&ba);
+            avatar.save(&bu, "PNG");
+            std::string avatarString = ba.toBase64().toStdString();
+            ui->messageView->setSenderImage(
+                accInfo->contactModel->getContactProfileId(contactUri),
+                avatarString);
+        }
+    } catch (...) { }
 }
 
 void
@@ -780,6 +791,7 @@ void
 CallWidget::backToWelcomePage()
 {
     deselectConversation();
+    ui->messageView->hideMessages();
     ui->stackedWidget->setCurrentWidget(ui->welcomePage);
 }
 
@@ -855,6 +867,7 @@ CallWidget::connectConversationModel()
     QObject::disconnect(conversationClearedConnection);
     QObject::disconnect(interactionStatusUpdatedConnection_);
     QObject::disconnect(newInteractionConnection_);
+    QObject::disconnect(interactionRemovedConnection_);
 
     modelSortedConnection_ = QObject::connect(
         currentConversationModel, &lrc::api::ConversationModel::modelSorted,
@@ -896,7 +909,7 @@ CallWidget::connectConversationModel()
     conversationClearedConnection = QObject::connect(
         currentConversationModel, &lrc::api::ConversationModel::conversationCleared,
         [this](const std::string& convUid) {
-            updateConversationView(convUid);
+            ui->messageView->clear();
             // if currently selected,
             // switch to welcome screen (deselecting current smartlist item )
             if (convUid != selectedConvUid()) {
@@ -907,11 +920,15 @@ CallWidget::connectConversationModel()
     );
     interactionStatusUpdatedConnection_ = QObject::connect(
         currentConversationModel, &lrc::api::ConversationModel::interactionStatusUpdated,
-        [this](const std::string& convUid) {
+        [this](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction) {
             if (convUid != selectedConvUid()) {
                 return;
             }
-            updateConversationView(convUid);
+            auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo();
+            auto currentConversationModel = currentAccountInfo.conversationModel.get();
+            currentConversationModel->clearUnreadInteractions(convUid);
+            ui->conversationsFilterWidget->update();
+            ui->messageView->updateInteraction(*currentConversationModel, interactionId, interaction);
         }
     );
     newInteractionConnection_ = QObject::connect(
@@ -920,6 +937,13 @@ CallWidget::connectConversationModel()
             onIncomingMessage(convUid, interactionId, interaction);
         }
     );
+    interactionRemovedConnection_ = QObject::connect(
+        currentConversationModel, &lrc::api::ConversationModel::interactionRemoved,
+        [this](const std::string& convUid, uint64_t interactionId) {
+            Q_UNUSED(convUid);
+            ui->messageView->removeInteraction(interactionId);
+        }
+    );
     currentConversationModel->setFilter("");
     // clear search field
     ui->ringContactLineEdit->setText("");
@@ -933,18 +957,8 @@ CallWidget::updateConversationView(const std::string& convUid)
         return;
     }
 
-    auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo();
-    auto currentConversationModel = currentAccountInfo.conversationModel.get();
-    currentConversationModel->clearUnreadInteractions(convUid);
-    ui->conversationsFilterWidget->update();
-    auto currentConversation = Utils::getConversationFromUid(convUid,
-                                                             *currentConversationModel);
-    if (currentConversation == currentConversationModel->allFilteredConversations().end()) {
-        return;
-    }
-    messageModel_.reset(new MessageModel(*currentConversation, currentAccountInfo, this->parent()));
-    ui->listMessageView->setModel(messageModel_.get());
-    ui->listMessageView->scrollToBottom();
+
+
 }
 
 void
@@ -959,6 +973,7 @@ CallWidget::selectConversation(const QModelIndex& index)
     const auto item = currentConversationModel->filteredConversation(index.row());
 
     if (selectConversation(item, *currentConversationModel)) {
+        showIMOutOfCall(index);
         auto convUid = selectedConvUid();
         if (!lastConvUid_.compare(convUid)) {
             return;
@@ -968,12 +983,12 @@ CallWidget::selectConversation(const QModelIndex& index)
         auto callModel = LRCInstance::getCurrentCallModel();
         auto conversation = Utils::getConversationFromUid(convUid, *currentConversationModel);
         const auto item = currentConversationModel->filteredConversation(index.row());
-        if (callModel->hasCall(conversation->callId)) {
-            ui->stackedWidget->setCurrentWidget(ui->videoPage);
-        }
-        else {
-            showIMOutOfCall(index);
+        if (callModel->hasCall(conversation->callId) && item.callId == conversation->callId) {
+            ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget);
+            ui->callStackWidget->show();
+            return;
         }
+        ui->callStackWidget->hide();
     }
 }
 
diff --git a/callwidget.h b/callwidget.h
index 7c37bcf..e6b1f13 100644
--- a/callwidget.h
+++ b/callwidget.h
@@ -30,7 +30,6 @@
 #include <QMovie>
 
 #include "navwidget.h"
-#include "instantmessagingwidget.h"
 #include "smartlistmodel.h"
 
 // old LRC
@@ -51,7 +50,6 @@
 #include "api/newcallmodel.h"
 
 class ConversationItemDelegate;
-class ImDelegate;
 class QPropertyAnimation;
 
 namespace Ui {
@@ -87,8 +85,6 @@ private slots:
     void on_refuseButton_clicked();
     void on_cancelButton_clicked();
     void on_smartList_doubleClicked(const QModelIndex& index);
-    void on_sendIMButton_clicked();
-    void on_imMessageEdit_returnPressed();
     void on_ringContactLineEdit_textChanged(const QString& text);
     void on_imBackButton_clicked();
     void on_sendContactRequestButton_clicked();
@@ -101,15 +97,12 @@ private slots:
 
 private slots:
     void smartListSelectionChanged(const QItemSelection  &selected, const QItemSelection  &deselected);
-    void contactReqListCurrentChanged(const QModelIndex &currentIdx, const QModelIndex &previousIdx);
-    void slotAccountMessageReceived(const QMap<QString,QString> message,ContactMethod* cm, media::Media::Direction dir);
     void onIncomingMessage(const std::string & convUid, uint64_t interactionId, const lrc::api::interaction::Info & interaction);
 
 private:
     void placeCall();
     void conversationsButtonClicked();
     void invitationsButtonClicked();
-    void setupOutOfCallIM();
     void setupSmartListContextMenu(const QPoint &pos);
     void setupQRCode(QString ringID);
     void backToWelcomePage();
@@ -136,14 +129,6 @@ private:
     const std::string& selectedConvUid();
 
     QMenu* menu_;
-    ConversationItemDelegate* conversationItemDelegate_;
-    ImDelegate* imDelegate_;
-
-    QMetaObject::Connection imConnection_;
-    QMetaObject::Connection imVisibleConnection_;
-    QMetaObject::Connection callChangedConnection_;
-    QMetaObject::Connection imClickedConnection_;
-    QMetaObject::Connection crListSelectionConnection_;
 
     Ui::CallWidget* ui;
     QMovie* miniSpinner_;
@@ -154,9 +139,13 @@ private:
     Video::Renderer* videoRenderer_;
     std::string lastConvUid_ {};
     lrc::api::profile::Type currentTypeFilter_{};
-
     std::unique_ptr<SmartListModel> smartListModel_;
-    std::unique_ptr<MessageModel> messageModel_;
+
+    QMetaObject::Connection imConnection_;
+    QMetaObject::Connection imVisibleConnection_;
+    QMetaObject::Connection callChangedConnection_;
+    QMetaObject::Connection imClickedConnection_;
+    QMetaObject::Connection crListSelectionConnection_;
     QMetaObject::Connection modelSortedConnection_;
     QMetaObject::Connection modelUpdatedConnection_;
     QMetaObject::Connection filterChangedConnection_;
@@ -167,4 +156,5 @@ private:
     QMetaObject::Connection conversationClearedConnection;
     QMetaObject::Connection selectedCallChanged_;
     QMetaObject::Connection smartlistSelectionConnection_;
+    QMetaObject::Connection interactionRemovedConnection_;
 };
diff --git a/callwidget.ui b/callwidget.ui
index 6b8f98b..dcc32b1 100644
--- a/callwidget.ui
+++ b/callwidget.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>746</width>
-    <height>639</height>
+    <width>1250</width>
+    <height>729</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -72,7 +72,7 @@
         </property>
         <property name="minimumSize">
          <size>
-          <width>324</width>
+          <width>320</width>
           <height>0</height>
          </size>
         </property>
@@ -93,256 +93,273 @@
           <number>0</number>
          </property>
          <item row="0" column="0">
-          <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
-           <property name="spacing">
-            <number>0</number>
-           </property>
-           <property name="sizeConstraint">
-            <enum>QLayout::SetDefaultConstraint</enum>
-           </property>
-           <property name="topMargin">
-            <number>4</number>
-           </property>
-           <item>
-            <widget class="CurrentAccountComboBox" name="currentAccountComboBox" native="true">
-             <property name="sizePolicy">
-              <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-               <horstretch>0</horstretch>
-               <verstretch>0</verstretch>
-              </sizepolicy>
-             </property>
-             <property name="minimumSize">
-              <size>
-               <width>0</width>
-               <height>60</height>
-              </size>
-             </property>
-             <property name="autoFillBackground">
-              <bool>false</bool>
-             </property>
-             <property name="styleSheet">
-              <string notr="true"/>
-             </property>
-            </widget>
-           </item>
-           <item>
-            <widget class="QWidget" name="selectBar" native="true">
-             <property name="sizePolicy">
-              <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
-               <horstretch>0</horstretch>
-               <verstretch>0</verstretch>
-              </sizepolicy>
-             </property>
-             <property name="maximumSize">
-              <size>
-               <width>16777214</width>
-               <height>30</height>
-              </size>
-             </property>
-             <layout class="QHBoxLayout" name="selectBar_layout" stretch="0,0,0">
-              <property name="spacing">
-               <number>0</number>
-              </property>
-              <property name="sizeConstraint">
-               <enum>QLayout::SetDefaultConstraint</enum>
-              </property>
-              <property name="leftMargin">
-               <number>0</number>
-              </property>
-              <property name="topMargin">
-               <number>0</number>
-              </property>
-              <property name="rightMargin">
-               <number>0</number>
-              </property>
-              <property name="bottomMargin">
-               <number>0</number>
+          <widget class="QWidget" name="sidePanelLayoutWidget" native="true">
+           <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
+            <property name="spacing">
+             <number>0</number>
+            </property>
+            <property name="sizeConstraint">
+             <enum>QLayout::SetDefaultConstraint</enum>
+            </property>
+            <property name="leftMargin">
+             <number>0</number>
+            </property>
+            <property name="topMargin">
+             <number>4</number>
+            </property>
+            <property name="rightMargin">
+             <number>0</number>
+            </property>
+            <property name="bottomMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="CurrentAccountComboBox" name="currentAccountComboBox" native="true">
+              <property name="sizePolicy">
+               <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+                <horstretch>0</horstretch>
+                <verstretch>0</verstretch>
+               </sizepolicy>
               </property>
-              <item>
-               <spacer name="horizontalSpacer">
-                <property name="orientation">
-                 <enum>Qt::Horizontal</enum>
-                </property>
-                <property name="sizeType">
-                 <enum>QSizePolicy::Fixed</enum>
-                </property>
-                <property name="sizeHint" stdset="0">
-                 <size>
-                  <width>10</width>
-                  <height>38</height>
-                 </size>
-                </property>
-               </spacer>
-              </item>
-              <item>
-               <widget class="RingContactLineEdit" name="ringContactLineEdit">
-                <property name="enabled">
-                 <bool>true</bool>
-                </property>
-                <property name="sizePolicy">
-                 <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-                  <horstretch>0</horstretch>
-                  <verstretch>0</verstretch>
-                 </sizepolicy>
-                </property>
-                <property name="minimumSize">
-                 <size>
-                  <width>266</width>
-                  <height>30</height>
-                 </size>
-                </property>
-                <property name="baseSize">
-                 <size>
-                  <width>244</width>
-                  <height>30</height>
-                 </size>
-                </property>
-                <property name="font">
-                 <font>
-                  <pointsize>9</pointsize>
-                 </font>
-                </property>
-                <property name="cursor">
-                 <cursorShape>IBeamCursor</cursorShape>
-                </property>
-                <property name="toolTip">
-                 <string>Search contact text input</string>
-                </property>
-                <property name="maxLength">
-                 <number>100</number>
-                </property>
-                <property name="alignment">
-                 <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
-                </property>
-                <property name="placeholderText">
-                 <string>Search contacts or enter ring ID</string>
-                </property>
-                <property name="clearButtonEnabled">
-                 <bool>true</bool>
-                </property>
-               </widget>
-              </item>
-              <item>
-               <spacer name="horizontalSpacer_2">
-                <property name="orientation">
-                 <enum>Qt::Horizontal</enum>
-                </property>
-                <property name="sizeType">
-                 <enum>QSizePolicy::Fixed</enum>
-                </property>
-                <property name="sizeHint" stdset="0">
-                 <size>
-                  <width>10</width>
-                  <height>38</height>
-                 </size>
-                </property>
-               </spacer>
-              </item>
-             </layout>
-            </widget>
-           </item>
-           <item>
-            <widget class="QWidget" name="smartListWidget" native="true">
-             <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0">
-              <property name="spacing">
-               <number>0</number>
+              <property name="minimumSize">
+               <size>
+                <width>0</width>
+                <height>60</height>
+               </size>
               </property>
-              <property name="leftMargin">
-               <number>1</number>
+              <property name="autoFillBackground">
+               <bool>false</bool>
               </property>
-              <property name="topMargin">
-               <number>1</number>
+              <property name="styleSheet">
+               <string notr="true"/>
               </property>
-              <property name="rightMargin">
-               <number>0</number>
+             </widget>
+            </item>
+            <item>
+             <widget class="QWidget" name="selectBar" native="true">
+              <property name="sizePolicy">
+               <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                <horstretch>0</horstretch>
+                <verstretch>0</verstretch>
+               </sizepolicy>
               </property>
-              <property name="bottomMargin">
-               <number>1</number>
+              <property name="maximumSize">
+               <size>
+                <width>16777214</width>
+                <height>30</height>
+               </size>
               </property>
-              <item>
-               <widget class="ConversationsFilterWidget" name="conversationsFilterWidget" native="true">
-                <layout class="QHBoxLayout" name="conversationFilterLayout">
-                 <property name="spacing">
-                  <number>10</number>
+              <layout class="QHBoxLayout" name="selectBar_layout" stretch="0,0,0">
+               <property name="spacing">
+                <number>0</number>
+               </property>
+               <property name="sizeConstraint">
+                <enum>QLayout::SetDefaultConstraint</enum>
+               </property>
+               <property name="leftMargin">
+                <number>0</number>
+               </property>
+               <property name="topMargin">
+                <number>0</number>
+               </property>
+               <property name="rightMargin">
+                <number>0</number>
+               </property>
+               <property name="bottomMargin">
+                <number>0</number>
+               </property>
+               <item>
+                <spacer name="horizontalSpacer">
+                 <property name="orientation">
+                  <enum>Qt::Horizontal</enum>
                  </property>
-                 <property name="leftMargin">
-                  <number>10</number>
+                 <property name="sizeType">
+                  <enum>QSizePolicy::Fixed</enum>
                  </property>
-                 <property name="topMargin">
-                  <number>10</number>
+                 <property name="sizeHint" stdset="0">
+                  <size>
+                   <width>10</width>
+                   <height>38</height>
+                  </size>
                  </property>
-                 <property name="rightMargin">
-                  <number>10</number>
+                </spacer>
+               </item>
+               <item>
+                <widget class="RingContactLineEdit" name="ringContactLineEdit">
+                 <property name="enabled">
+                  <bool>true</bool>
+                 </property>
+                 <property name="sizePolicy">
+                  <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+                   <horstretch>0</horstretch>
+                   <verstretch>0</verstretch>
+                  </sizepolicy>
+                 </property>
+                 <property name="minimumSize">
+                  <size>
+                   <width>266</width>
+                   <height>30</height>
+                  </size>
+                 </property>
+                 <property name="baseSize">
+                  <size>
+                   <width>244</width>
+                   <height>30</height>
+                  </size>
+                 </property>
+                 <property name="font">
+                  <font>
+                   <pointsize>9</pointsize>
+                  </font>
+                 </property>
+                 <property name="cursor">
+                  <cursorShape>IBeamCursor</cursorShape>
+                 </property>
+                 <property name="toolTip">
+                  <string>Search contact text input</string>
+                 </property>
+                 <property name="maxLength">
+                  <number>100</number>
                  </property>
-                 <property name="bottomMargin">
+                 <property name="alignment">
+                  <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+                 </property>
+                 <property name="placeholderText">
+                  <string>Search contacts or enter ring ID</string>
+                 </property>
+                 <property name="clearButtonEnabled">
+                  <bool>true</bool>
+                 </property>
+                </widget>
+               </item>
+               <item>
+                <spacer name="horizontalSpacer_2">
+                 <property name="orientation">
+                  <enum>Qt::Horizontal</enum>
+                 </property>
+                 <property name="sizeType">
+                  <enum>QSizePolicy::Fixed</enum>
+                 </property>
+                 <property name="sizeHint" stdset="0">
+                  <size>
+                   <width>10</width>
+                   <height>38</height>
+                  </size>
+                 </property>
+                </spacer>
+               </item>
+              </layout>
+             </widget>
+            </item>
+            <item>
+             <widget class="QWidget" name="smartListWidget" native="true">
+              <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0">
+               <property name="spacing">
+                <number>0</number>
+               </property>
+               <property name="leftMargin">
+                <number>1</number>
+               </property>
+               <property name="topMargin">
+                <number>1</number>
+               </property>
+               <property name="rightMargin">
+                <number>0</number>
+               </property>
+               <property name="bottomMargin">
+                <number>1</number>
+               </property>
+               <item>
+                <widget class="ConversationsFilterWidget" name="conversationsFilterWidget" native="true">
+                 <layout class="QHBoxLayout" name="conversationFilterLayout">
+                  <property name="spacing">
+                   <number>10</number>
+                  </property>
+                  <property name="leftMargin">
+                   <number>10</number>
+                  </property>
+                  <property name="topMargin">
+                   <number>10</number>
+                  </property>
+                  <property name="rightMargin">
+                   <number>10</number>
+                  </property>
+                  <property name="bottomMargin">
+                   <number>0</number>
+                  </property>
+                  <item>
+                   <widget class="QPushButton" name="btnConversations">
+                    <property name="toolTip">
+                     <string>Show conversations</string>
+                    </property>
+                    <property name="text">
+                     <string>Conversations</string>
+                    </property>
+                    <property name="checkable">
+                     <bool>true</bool>
+                    </property>
+                   </widget>
+                  </item>
+                  <item>
+                   <widget class="QPushButton" name="btnInvites">
+                    <property name="toolTip">
+                     <string>Show invites</string>
+                    </property>
+                    <property name="text">
+                     <string>Invites</string>
+                    </property>
+                    <property name="checkable">
+                     <bool>true</bool>
+                    </property>
+                   </widget>
+                  </item>
+                 </layout>
+                </widget>
+               </item>
+               <item>
+                <layout class="QVBoxLayout" name="verticalLayout_13">
+                 <property name="spacing">
                   <number>0</number>
                  </property>
+                 <property name="topMargin">
+                  <number>10</number>
+                 </property>
                  <item>
-                  <widget class="ConversationFilterButton" name="buttonConversations" native="true">
-                   <property name="toolTip">
-                    <string>Show conversations</string>
+                  <widget class="SmartListView" name="smartList">
+                   <property name="autoScrollMargin">
+                    <number>16</number>
                    </property>
-                   <property name="text" stdset="0">
-                    <string>Conversations</string>
+                   <property name="indentation">
+                    <number>0</number>
                    </property>
-                  </widget>
-                 </item>
-                 <item>
-                  <widget class="ConversationFilterButton" name="buttonInvites" native="true">
-                   <property name="toolTip">
-                    <string>Show invites</string>
+                   <property name="rootIsDecorated">
+                    <bool>false</bool>
                    </property>
-                   <property name="text" stdset="0">
-                    <string>Invites</string>
+                   <property name="itemsExpandable">
+                    <bool>false</bool>
                    </property>
+                   <property name="expandsOnDoubleClick">
+                    <bool>false</bool>
+                   </property>
+                   <attribute name="headerVisible">
+                    <bool>false</bool>
+                   </attribute>
                   </widget>
                  </item>
                 </layout>
-               </widget>
-              </item>
-              <item>
-               <layout class="QVBoxLayout" name="verticalLayout_13">
-                <property name="spacing">
-                 <number>0</number>
-                </property>
-                <property name="topMargin">
-                 <number>10</number>
-                </property>
-                <item>
-                 <widget class="SmartListView" name="smartList">
-                  <property name="autoScrollMargin">
-                   <number>16</number>
-                  </property>
-                  <property name="indentation">
-                   <number>0</number>
-                  </property>
-                  <property name="rootIsDecorated">
-                   <bool>false</bool>
-                  </property>
-                  <property name="itemsExpandable">
-                   <bool>false</bool>
-                  </property>
-                  <property name="expandsOnDoubleClick">
-                   <bool>false</bool>
-                  </property>
-                  <attribute name="headerVisible">
-                   <bool>false</bool>
-                  </attribute>
-                 </widget>
-                </item>
-               </layout>
-              </item>
-             </layout>
-            </widget>
-           </item>
-          </layout>
+               </item>
+              </layout>
+             </widget>
+            </item>
+           </layout>
+          </widget>
          </item>
         </layout>
        </widget>
        <widget class="QWidget" name="widgetSplitterRight" native="true">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-          <horstretch>1</horstretch>
+          <horstretch>0</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
@@ -366,7 +383,7 @@
           <number>0</number>
          </property>
          <item row="0" column="0">
-          <widget class="QWidget" name="CallSubGroup" native="true">
+          <widget class="QWidget" name="mainLayoutWidget" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
              <horstretch>0</horstretch>
@@ -404,7 +421,7 @@
                <string/>
               </property>
               <property name="currentIndex">
-               <number>2</number>
+               <number>0</number>
               </property>
               <widget class="QWidget" name="welcomePage">
                <layout class="QVBoxLayout" name="verticalLayout_15">
@@ -423,70 +440,6 @@
                 <property name="bottomMargin">
                  <number>0</number>
                 </property>
-                <item>
-                 <layout class="QHBoxLayout" name="horizontalLayout_9">
-                  <property name="spacing">
-                   <number>0</number>
-                  </property>
-                  <property name="topMargin">
-                   <number>7</number>
-                  </property>
-                  <property name="rightMargin">
-                   <number>7</number>
-                  </property>
-                  <item>
-                   <spacer name="horizontalSpacer_11">
-                    <property name="orientation">
-                     <enum>Qt::Horizontal</enum>
-                    </property>
-                    <property name="sizeHint" stdset="0">
-                     <size>
-                      <width>40</width>
-                      <height>20</height>
-                     </size>
-                    </property>
-                   </spacer>
-                  </item>
-                  <item>
-                   <widget class="QPushButton" name="settingsButton">
-                    <property name="sizePolicy">
-                     <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
-                      <horstretch>0</horstretch>
-                      <verstretch>0</verstretch>
-                     </sizepolicy>
-                    </property>
-                    <property name="minimumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="maximumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="toolTip">
-                     <string>Configuration menu</string>
-                    </property>
-                    <property name="text">
-                     <string/>
-                    </property>
-                    <property name="icon">
-                     <iconset resource="ressources.qrc">
-                      <normaloff>:/images/icons/ic_settings_white_48dp_2x.png</normaloff>:/images/icons/ic_settings_white_48dp_2x.png</iconset>
-                    </property>
-                    <property name="iconSize">
-                     <size>
-                      <width>18</width>
-                      <height>18</height>
-                     </size>
-                    </property>
-                   </widget>
-                  </item>
-                 </layout>
-                </item>
                 <item>
                  <spacer name="verticalSpacer">
                   <property name="orientation">
@@ -592,8 +545,8 @@
                   </property>
                   <property name="text">
                    <string>
-                                        This is your RingID.
-                                        Copy and share it with your friends!
+ This is your RingID.
+Copy and share it with your friends!
                                       </string>
                   </property>
                   <property name="textFormat">
@@ -870,426 +823,346 @@
                 </item>
                </layout>
               </widget>
-              <widget class="QWidget" name="sendContactRequestPage">
-               <layout class="QVBoxLayout" name="verticalLayout_7">
-                <item>
-                 <layout class="QHBoxLayout" name="horizontalLayout_10">
-                  <property name="bottomMargin">
-                   <number>0</number>
+              <widget class="QWidget" name="mainActivityWidget">
+               <layout class="QGridLayout" name="gridLayout_5">
+                <property name="leftMargin">
+                 <number>0</number>
+                </property>
+                <property name="topMargin">
+                 <number>0</number>
+                </property>
+                <property name="rightMargin">
+                 <number>0</number>
+                </property>
+                <property name="bottomMargin">
+                 <number>0</number>
+                </property>
+                <property name="spacing">
+                 <number>0</number>
+                </property>
+                <item row="0" column="0">
+                 <widget class="QSplitter" name="mainActivitySplitter">
+                  <property name="sizePolicy">
+                   <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+                    <horstretch>0</horstretch>
+                    <verstretch>0</verstretch>
+                   </sizepolicy>
                   </property>
-                  <item>
-                   <widget class="QPushButton" name="sendCRBackButton">
-                    <property name="minimumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="maximumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="toolTip">
-                     <string>Back to homepage button</string>
-                    </property>
-                    <property name="text">
-                     <string/>
-                    </property>
-                    <property name="icon">
-                     <iconset resource="ressources.qrc">
-                      <normaloff>:/images/icons/ic_arrow_back_white_24dp.png</normaloff>:/images/icons/ic_arrow_back_white_24dp.png</iconset>
-                    </property>
-                    <property name="iconSize">
-                     <size>
-                      <width>18</width>
-                      <height>18</height>
-                     </size>
-                    </property>
-                    <property name="flat">
-                     <bool>false</bool>
-                    </property>
-                   </widget>
-                  </item>
-                  <item>
-                   <spacer name="horizontalSpacer_14">
-                    <property name="orientation">
-                     <enum>Qt::Horizontal</enum>
-                    </property>
-                    <property name="sizeHint" stdset="0">
-                     <size>
-                      <width>40</width>
-                      <height>20</height>
-                     </size>
-                    </property>
-                   </spacer>
-                  </item>
-                 </layout>
-                </item>
-                <item>
-                 <widget class="SendContactRequestWidget" name="sendContactRequestWidget" native="true">
-                  <property name="sizePolicy">
-                   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
-                    <horstretch>0</horstretch>
-                    <verstretch>0</verstretch>
-                   </sizepolicy>
+                  <property name="orientation">
+                   <enum>Qt::Horizontal</enum>
                   </property>
-                  <property name="locale">
-                   <locale language="English" country="UnitedStates"/>
+                  <property name="handleWidth">
+                   <number>2</number>
                   </property>
-                 </widget>
-                </item>
-               </layout>
-              </widget>
-              <widget class="QWidget" name="messagingPage">
-               <property name="sizePolicy">
-                <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
-                 <horstretch>0</horstretch>
-                 <verstretch>0</verstretch>
-                </sizepolicy>
-               </property>
-               <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
-                <property name="spacing">
-                 <number>0</number>
-                </property>
-                <property name="leftMargin">
-                 <number>0</number>
-                </property>
-                <property name="topMargin">
-                 <number>0</number>
-                </property>
-                <property name="rightMargin">
-                 <number>0</number>
-                </property>
-                <property name="bottomMargin">
-                 <number>0</number>
-                </property>
-                <item>
-                 <widget class="QWidget" name="messagingHeaderWidget" native="true">
-                  <layout class="QHBoxLayout" name="horizontalLayout_6">
-                   <property name="spacing">
-                    <number>6</number>
-                   </property>
-                   <property name="leftMargin">
-                    <number>10</number>
-                   </property>
-                   <property name="topMargin">
-                    <number>6</number>
-                   </property>
-                   <property name="rightMargin">
-                    <number>10</number>
+                  <property name="childrenCollapsible">
+                   <bool>true</bool>
+                  </property>
+                  <widget class="QStackedWidget" name="callStackWidget">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
                    </property>
-                   <property name="bottomMargin">
-                    <number>6</number>
+                   <property name="currentIndex">
+                    <number>1</number>
                    </property>
-                   <item>
-                    <widget class="QPushButton" name="imBackButton">
-                     <property name="sizePolicy">
-                      <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-                       <horstretch>0</horstretch>
-                       <verstretch>0</verstretch>
-                      </sizepolicy>
-                     </property>
-                     <property name="minimumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="maximumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="toolTip">
-                      <string>Back to homepage button</string>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                     <property name="icon">
-                      <iconset resource="ressources.qrc">
-                       <normaloff>:/images/icons/ic_arrow_back_24px.svg</normaloff>:/images/icons/ic_arrow_back_24px.svg</iconset>
-                     </property>
-                     <property name="iconSize">
-                      <size>
-                       <width>18</width>
-                       <height>18</height>
-                      </size>
-                     </property>
-                     <property name="flat">
-                      <bool>false</bool>
+                   <widget class="QWidget" name="videoPage">
+                    <property name="sizePolicy">
+                     <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                      <horstretch>0</horstretch>
+                      <verstretch>0</verstretch>
+                     </sizepolicy>
+                    </property>
+                    <layout class="QGridLayout" name="gridLayout_6">
+                     <property name="leftMargin">
+                      <number>0</number>
                      </property>
-                    </widget>
-                   </item>
-                   <item>
-                    <layout class="QVBoxLayout" name="verticalLayout_14">
-                     <property name="spacing">
+                     <property name="topMargin">
                       <number>0</number>
                      </property>
-                     <property name="leftMargin">
+                     <property name="rightMargin">
                       <number>0</number>
                      </property>
                      <property name="bottomMargin">
                       <number>0</number>
                      </property>
-                     <item>
-                      <widget class="QLabel" name="imNameLabel">
+                     <property name="spacing">
+                      <number>0</number>
+                     </property>
+                     <item row="0" column="0">
+                      <widget class="VideoView" name="videoWidget" native="true">
                        <property name="sizePolicy">
                         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
                          <horstretch>0</horstretch>
                          <verstretch>0</verstretch>
                         </sizepolicy>
                        </property>
-                       <property name="minimumSize">
-                        <size>
-                         <width>200</width>
-                         <height>0</height>
-                        </size>
-                       </property>
-                       <property name="font">
-                        <font>
-                         <pointsize>10</pointsize>
-                         <weight>75</weight>
-                         <bold>true</bold>
-                        </font>
-                       </property>
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(63,63,63);</string>
-                       </property>
-                       <property name="text">
-                        <string/>
-                       </property>
-                       <property name="alignment">
-                        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
-                       </property>
-                       <property name="wordWrap">
-                        <bool>false</bool>
-                       </property>
                       </widget>
                      </item>
-                     <item>
-                      <widget class="QLabel" name="imIdLabel">
+                    </layout>
+                   </widget>
+                   <widget class="QWidget" name="outgoingCallPage">
+                    <property name="sizePolicy">
+                     <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                      <horstretch>0</horstretch>
+                      <verstretch>0</verstretch>
+                     </sizepolicy>
+                    </property>
+                    <layout class="QGridLayout" name="gridLayout_7">
+                     <property name="leftMargin">
+                      <number>0</number>
+                     </property>
+                     <property name="topMargin">
+                      <number>0</number>
+                     </property>
+                     <property name="rightMargin">
+                      <number>0</number>
+                     </property>
+                     <property name="bottomMargin">
+                      <number>0</number>
+                     </property>
+                     <property name="spacing">
+                      <number>0</number>
+                     </property>
+                     <item row="0" column="0">
+                      <widget class="QWidget" name="outgoingCall" native="true">
                        <property name="sizePolicy">
-                        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+                        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
                          <horstretch>0</horstretch>
                          <verstretch>0</verstretch>
                         </sizepolicy>
                        </property>
                        <property name="minimumSize">
                         <size>
-                         <width>200</width>
-                         <height>0</height>
+                         <width>244</width>
+                         <height>420</height>
                         </size>
                        </property>
-                       <property name="font">
-                        <font>
-                         <pointsize>9</pointsize>
-                        </font>
-                       </property>
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(192,192,192);</string>
-                       </property>
-                       <property name="text">
-                        <string/>
+                       <property name="maximumSize">
+                        <size>
+                         <width>244</width>
+                         <height>16777215</height>
+                        </size>
                        </property>
-                       <property name="alignment">
-                        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+                       <property name="baseSize">
+                        <size>
+                         <width>0</width>
+                         <height>0</height>
+                        </size>
                        </property>
+                       <layout class="QVBoxLayout" name="spinnerLayout_3">
+                        <property name="spacing">
+                         <number>10</number>
+                        </property>
+                        <property name="leftMargin">
+                         <number>0</number>
+                        </property>
+                        <property name="topMargin">
+                         <number>0</number>
+                        </property>
+                        <property name="rightMargin">
+                         <number>0</number>
+                        </property>
+                        <property name="bottomMargin">
+                         <number>0</number>
+                        </property>
+                        <item>
+                         <layout class="QHBoxLayout" name="horizontalLayout_3">
+                          <property name="bottomMargin">
+                           <number>0</number>
+                          </property>
+                          <item>
+                           <widget class="QLabel" name="callingPhoto">
+                            <property name="sizePolicy">
+                             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+                              <horstretch>0</horstretch>
+                              <verstretch>0</verstretch>
+                             </sizepolicy>
+                            </property>
+                            <property name="minimumSize">
+                             <size>
+                              <width>166</width>
+                              <height>166</height>
+                             </size>
+                            </property>
+                            <property name="maximumSize">
+                             <size>
+                              <width>166</width>
+                              <height>166</height>
+                             </size>
+                            </property>
+                            <property name="baseSize">
+                             <size>
+                              <width>166</width>
+                              <height>166</height>
+                             </size>
+                            </property>
+                            <property name="text">
+                             <string/>
+                            </property>
+                            <property name="alignment">
+                             <set>Qt::AlignCenter</set>
+                            </property>
+                           </widget>
+                          </item>
+                         </layout>
+                        </item>
+                        <item>
+                         <widget class="QLabel" name="callingBestNameLabel">
+                          <property name="font">
+                           <font>
+                            <family>Segoe UI Emoji</family>
+                            <pointsize>12</pointsize>
+                           </font>
+                          </property>
+                          <property name="styleSheet">
+                           <string notr="true">color: rgb(128, 128, 128);</string>
+                          </property>
+                          <property name="text">
+                           <string>best name</string>
+                          </property>
+                          <property name="alignment">
+                           <set>Qt::AlignCenter</set>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <widget class="QLabel" name="callingBestIdLabel">
+                          <property name="sizePolicy">
+                           <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+                            <horstretch>0</horstretch>
+                            <verstretch>0</verstretch>
+                           </sizepolicy>
+                          </property>
+                          <property name="font">
+                           <font>
+                            <pointsize>8</pointsize>
+                           </font>
+                          </property>
+                          <property name="styleSheet">
+                           <string notr="true">color: rgb(174, 174, 174);</string>
+                          </property>
+                          <property name="text">
+                           <string>best Id</string>
+                          </property>
+                          <property name="alignment">
+                           <set>Qt::AlignCenter</set>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <widget class="QLabel" name="spinnerLabel">
+                          <property name="sizePolicy">
+                           <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                            <horstretch>0</horstretch>
+                            <verstretch>0</verstretch>
+                           </sizepolicy>
+                          </property>
+                          <property name="minimumSize">
+                           <size>
+                            <width>20</width>
+                            <height>0</height>
+                           </size>
+                          </property>
+                          <property name="text">
+                           <string/>
+                          </property>
+                          <property name="alignment">
+                           <set>Qt::AlignCenter</set>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <layout class="QVBoxLayout" name="verticalLayout_6">
+                          <property name="spacing">
+                           <number>20</number>
+                          </property>
+                          <property name="bottomMargin">
+                           <number>0</number>
+                          </property>
+                          <item>
+                           <layout class="QHBoxLayout" name="horizontalLayout">
+                            <property name="spacing">
+                             <number>0</number>
+                            </property>
+                            <property name="bottomMargin">
+                             <number>0</number>
+                            </property>
+                            <item>
+                             <widget class="QPushButton" name="cancelButton">
+                              <property name="sizePolicy">
+                               <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+                                <horstretch>0</horstretch>
+                                <verstretch>0</verstretch>
+                               </sizepolicy>
+                              </property>
+                              <property name="minimumSize">
+                               <size>
+                                <width>56</width>
+                                <height>56</height>
+                               </size>
+                              </property>
+                              <property name="maximumSize">
+                               <size>
+                                <width>56</width>
+                                <height>56</height>
+                               </size>
+                              </property>
+                              <property name="baseSize">
+                               <size>
+                                <width>56</width>
+                                <height>56</height>
+                               </size>
+                              </property>
+                              <property name="toolTip">
+                               <string>Cancel outgoing call</string>
+                              </property>
+                              <property name="layoutDirection">
+                               <enum>Qt::LeftToRight</enum>
+                              </property>
+                              <property name="icon">
+                               <iconset resource="ressources.qrc">
+                                <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset>
+                              </property>
+                              <property name="iconSize">
+                               <size>
+                                <width>24</width>
+                                <height>24</height>
+                               </size>
+                              </property>
+                             </widget>
+                            </item>
+                           </layout>
+                          </item>
+                          <item>
+                           <widget class="QLabel" name="cancelCallLabel">
+                            <property name="styleSheet">
+                             <string notr="true">color: rgb(174, 174, 174);</string>
+                            </property>
+                            <property name="text">
+                             <string>Cancel</string>
+                            </property>
+                            <property name="alignment">
+                             <set>Qt::AlignCenter</set>
+                            </property>
+                           </widget>
+                          </item>
+                         </layout>
+                        </item>
+                       </layout>
                       </widget>
                      </item>
                     </layout>
-                   </item>
-                   <item>
-                    <spacer name="verticalSpacer_3">
-                     <property name="orientation">
-                      <enum>Qt::Vertical</enum>
-                     </property>
-                     <property name="sizeType">
-                      <enum>QSizePolicy::Fixed</enum>
-                     </property>
-                     <property name="sizeHint" stdset="0">
-                      <size>
-                       <width>0</width>
-                       <height>40</height>
-                      </size>
-                     </property>
-                    </spacer>
-                   </item>
-                   <item>
-                    <spacer name="horizontalSpacer_6">
-                     <property name="orientation">
-                      <enum>Qt::Horizontal</enum>
-                     </property>
-                     <property name="sizeHint" stdset="0">
-                      <size>
-                       <width>40</width>
-                       <height>20</height>
-                      </size>
-                     </property>
-                    </spacer>
-                   </item>
-                   <item>
-                    <widget class="QPushButton" name="btnAudioCall">
-                     <property name="minimumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="maximumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                     <property name="icon">
-                      <iconset resource="ressources.qrc">
-                       <normaloff>:/images/icons/ic_phone_24px.svg</normaloff>:/images/icons/ic_phone_24px.svg</iconset>
-                     </property>
-                     <property name="iconSize">
-                      <size>
-                       <width>18</width>
-                       <height>18</height>
-                      </size>
-                     </property>
-                    </widget>
-                   </item>
-                   <item>
-                    <widget class="QPushButton" name="btnVideoCall">
-                     <property name="minimumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="maximumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                     <property name="icon">
-                      <iconset resource="ressources.qrc">
-                       <normaloff>:/images/icons/ic_video_call_24px.svg</normaloff>:/images/icons/ic_video_call_24px.svg</iconset>
-                     </property>
-                     <property name="iconSize">
-                      <size>
-                       <width>18</width>
-                       <height>18</height>
-                      </size>
-                     </property>
-                    </widget>
-                   </item>
-                   <item>
-                    <widget class="QPushButton" name="sendContactRequestButton">
-                     <property name="sizePolicy">
-                      <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-                       <horstretch>0</horstretch>
-                       <verstretch>0</verstretch>
-                      </sizepolicy>
-                     </property>
-                     <property name="minimumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="maximumSize">
-                      <size>
-                       <width>30</width>
-                       <height>30</height>
-                      </size>
-                     </property>
-                     <property name="toolTip">
-                      <string>Add to contacts</string>
-                     </property>
-                     <property name="styleSheet">
-                      <string notr="true"/>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                     <property name="icon">
-                      <iconset resource="ressources.qrc">
-                       <normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</iconset>
-                     </property>
-                     <property name="iconSize">
-                      <size>
-                       <width>18</width>
-                       <height>18</height>
-                      </size>
-                     </property>
-                    </widget>
-                   </item>
-                  </layout>
-                 </widget>
-                </item>
-                <item>
-                 <layout class="QHBoxLayout" name="messagesHBoxLayout" stretch="0,0,0">
-                  <property name="spacing">
-                   <number>0</number>
-                  </property>
-                  <property name="leftMargin">
-                   <number>10</number>
-                  </property>
-                  <property name="topMargin">
-                   <number>0</number>
-                  </property>
-                  <property name="rightMargin">
-                   <number>10</number>
-                  </property>
-                  <property name="bottomMargin">
-                   <number>0</number>
-                  </property>
-                  <item>
-                   <spacer name="horizontalSpacer">
-                    <property name="orientation">
-                     <enum>Qt::Horizontal</enum>
-                    </property>
-                    <property name="sizeType">
-                     <enum>QSizePolicy::Minimum</enum>
-                    </property>
-                    <property name="sizeHint" stdset="0">
-                     <size>
-                      <width>0</width>
-                      <height>20</height>
-                     </size>
-                    </property>
-                   </spacer>
-                  </item>
-                  <item>
-                   <widget class="QWidget" name="messagesListWidget" native="true">
+                   </widget>
+                   <widget class="QWidget" name="incomingCallPage">
                     <property name="sizePolicy">
-                     <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+                     <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
                       <horstretch>0</horstretch>
                       <verstretch>0</verstretch>
                      </sizepolicy>
                     </property>
-                    <property name="minimumSize">
-                     <size>
-                      <width>200</width>
-                      <height>0</height>
-                     </size>
-                    </property>
-                    <property name="maximumSize">
-                     <size>
-                      <width>640</width>
-                      <height>16777215</height>
-                     </size>
-                    </property>
-                    <layout class="QVBoxLayout" name="verticalLayout_6">
-                     <property name="spacing">
-                      <number>0</number>
-                     </property>
+                    <layout class="QGridLayout" name="gridLayout_8">
                      <property name="leftMargin">
                       <number>0</number>
                      </property>
@@ -1302,10 +1175,13 @@
                      <property name="bottomMargin">
                       <number>0</number>
                      </property>
-                     <item>
-                      <widget class="QListView" name="listMessageView">
+                     <property name="spacing">
+                      <number>0</number>
+                     </property>
+                     <item row="0" column="0">
+                      <widget class="QWidget" name="callInvite" native="true">
                        <property name="sizePolicy">
-                        <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+                        <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
                          <horstretch>0</horstretch>
                          <verstretch>0</verstretch>
                         </sizepolicy>
@@ -1313,86 +1189,565 @@
                        <property name="minimumSize">
                         <size>
                          <width>200</width>
-                         <height>0</height>
+                         <height>420</height>
                         </size>
                        </property>
                        <property name="maximumSize">
                         <size>
-                         <width>640</width>
-                         <height>16777215</height>
+                         <width>200</width>
+                         <height>420</height>
                         </size>
                        </property>
-                       <property name="font">
-                        <font>
-                         <stylestrategy>PreferAntialias</stylestrategy>
-                        </font>
-                       </property>
-                       <property name="focusPolicy">
-                        <enum>Qt::NoFocus</enum>
-                       </property>
-                       <property name="frameShape">
-                        <enum>QFrame::NoFrame</enum>
-                       </property>
-                       <property name="frameShadow">
-                        <enum>QFrame::Sunken</enum>
-                       </property>
-                       <property name="horizontalScrollBarPolicy">
-                        <enum>Qt::ScrollBarAsNeeded</enum>
-                       </property>
-                       <property name="verticalScrollMode">
-                        <enum>QAbstractItemView::ScrollPerPixel</enum>
-                       </property>
-                       <property name="horizontalScrollMode">
-                        <enum>QAbstractItemView::ScrollPerPixel</enum>
-                       </property>
-                       <property name="spacing">
-                        <number>1</number>
-                       </property>
-                       <property name="wordWrap">
-                        <bool>true</bool>
-                       </property>
-                       <property name="selectionRectVisible">
-                        <bool>false</bool>
+                       <property name="baseSize">
+                        <size>
+                         <width>200</width>
+                         <height>420</height>
+                        </size>
                        </property>
+                       <layout class="QVBoxLayout" name="callInvite" stretch="0,2,1,2,1,9">
+                        <property name="spacing">
+                         <number>0</number>
+                        </property>
+                        <item>
+                         <widget class="QLabel" name="callerPhoto">
+                          <property name="sizePolicy">
+                           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+                            <horstretch>0</horstretch>
+                            <verstretch>0</verstretch>
+                           </sizepolicy>
+                          </property>
+                          <property name="minimumSize">
+                           <size>
+                            <width>166</width>
+                            <height>166</height>
+                           </size>
+                          </property>
+                          <property name="maximumSize">
+                           <size>
+                            <width>166</width>
+                            <height>166</height>
+                           </size>
+                          </property>
+                          <property name="baseSize">
+                           <size>
+                            <width>166</width>
+                            <height>166</height>
+                           </size>
+                          </property>
+                          <property name="text">
+                           <string/>
+                          </property>
+                          <property name="alignment">
+                           <set>Qt::AlignCenter</set>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <spacer name="verticalSpacer_3">
+                          <property name="orientation">
+                           <enum>Qt::Vertical</enum>
+                          </property>
+                          <property name="sizeType">
+                           <enum>QSizePolicy::Fixed</enum>
+                          </property>
+                          <property name="sizeHint" stdset="0">
+                           <size>
+                            <width>20</width>
+                            <height>20</height>
+                           </size>
+                          </property>
+                         </spacer>
+                        </item>
+                        <item alignment="Qt::AlignHCenter">
+                         <widget class="QLabel" name="callerBestNameLabel">
+                          <property name="font">
+                           <font>
+                            <family>Segoe UI Emoji</family>
+                            <pointsize>12</pointsize>
+                           </font>
+                          </property>
+                          <property name="styleSheet">
+                           <string notr="true">color: rgb(128, 128, 128);</string>
+                          </property>
+                          <property name="text">
+                           <string>best name</string>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <widget class="QLabel" name="wantToTalkLabel">
+                          <property name="styleSheet">
+                           <string notr="true">color: rgb(174, 174, 174);</string>
+                          </property>
+                          <property name="text">
+                           <string>Wants to talk to you!</string>
+                          </property>
+                          <property name="alignment">
+                           <set>Qt::AlignCenter</set>
+                          </property>
+                         </widget>
+                        </item>
+                        <item>
+                         <spacer name="verticalSpacer_5">
+                          <property name="orientation">
+                           <enum>Qt::Vertical</enum>
+                          </property>
+                          <property name="sizeType">
+                           <enum>QSizePolicy::Fixed</enum>
+                          </property>
+                          <property name="sizeHint" stdset="0">
+                           <size>
+                            <width>20</width>
+                            <height>50</height>
+                           </size>
+                          </property>
+                         </spacer>
+                        </item>
+                        <item>
+                         <layout class="QVBoxLayout" name="verticalLayout_3" stretch="2">
+                          <property name="spacing">
+                           <number>0</number>
+                          </property>
+                          <property name="rightMargin">
+                           <number>0</number>
+                          </property>
+                          <property name="bottomMargin">
+                           <number>0</number>
+                          </property>
+                          <item>
+                           <layout class="QHBoxLayout" name="horizontalLayout_2">
+                            <property name="spacing">
+                             <number>0</number>
+                            </property>
+                            <property name="topMargin">
+                             <number>0</number>
+                            </property>
+                            <property name="rightMargin">
+                             <number>0</number>
+                            </property>
+                            <item>
+                             <layout class="QVBoxLayout" name="verticalLayout_4">
+                              <property name="spacing">
+                               <number>0</number>
+                              </property>
+                              <property name="rightMargin">
+                               <number>0</number>
+                              </property>
+                              <item>
+                               <layout class="QHBoxLayout" name="horizontalLayout_4">
+                                <property name="bottomMargin">
+                                 <number>0</number>
+                                </property>
+                                <item>
+                                 <widget class="QPushButton" name="acceptButton">
+                                  <property name="sizePolicy">
+                                   <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+                                    <horstretch>0</horstretch>
+                                    <verstretch>0</verstretch>
+                                   </sizepolicy>
+                                  </property>
+                                  <property name="minimumSize">
+                                   <size>
+                                    <width>56</width>
+                                    <height>56</height>
+                                   </size>
+                                  </property>
+                                  <property name="toolTip">
+                                   <string>Answer incoming call button</string>
+                                  </property>
+                                  <property name="icon">
+                                   <iconset resource="ressources.qrc">
+                                    <normaloff>:/images/icons/ic_done_white_24dp.png</normaloff>:/images/icons/ic_done_white_24dp.png</iconset>
+                                  </property>
+                                  <property name="iconSize">
+                                   <size>
+                                    <width>24</width>
+                                    <height>24</height>
+                                   </size>
+                                  </property>
+                                 </widget>
+                                </item>
+                               </layout>
+                              </item>
+                              <item>
+                               <widget class="QLabel" name="acceptLabel">
+                                <property name="styleSheet">
+                                 <string notr="true">color: rgb(174, 174, 174);</string>
+                                </property>
+                                <property name="text">
+                                 <string>Answer</string>
+                                </property>
+                                <property name="alignment">
+                                 <set>Qt::AlignCenter</set>
+                                </property>
+                               </widget>
+                              </item>
+                             </layout>
+                            </item>
+                            <item>
+                             <spacer name="horizontalSpacer_21">
+                              <property name="orientation">
+                               <enum>Qt::Horizontal</enum>
+                              </property>
+                              <property name="sizeType">
+                               <enum>QSizePolicy::Fixed</enum>
+                              </property>
+                              <property name="sizeHint" stdset="0">
+                               <size>
+                                <width>40</width>
+                                <height>20</height>
+                               </size>
+                              </property>
+                             </spacer>
+                            </item>
+                            <item>
+                             <layout class="QVBoxLayout" name="verticalLayout_7">
+                              <property name="spacing">
+                               <number>0</number>
+                              </property>
+                              <property name="bottomMargin">
+                               <number>0</number>
+                              </property>
+                              <item>
+                               <layout class="QHBoxLayout" name="horizontalLayout_5">
+                                <item>
+                                 <widget class="QPushButton" name="refuseButton">
+                                  <property name="sizePolicy">
+                                   <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+                                    <horstretch>0</horstretch>
+                                    <verstretch>0</verstretch>
+                                   </sizepolicy>
+                                  </property>
+                                  <property name="minimumSize">
+                                   <size>
+                                    <width>56</width>
+                                    <height>56</height>
+                                   </size>
+                                  </property>
+                                  <property name="toolTip">
+                                   <string>Ignore incoming call button</string>
+                                  </property>
+                                  <property name="layoutDirection">
+                                   <enum>Qt::RightToLeft</enum>
+                                  </property>
+                                  <property name="icon">
+                                   <iconset resource="ressources.qrc">
+                                    <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset>
+                                  </property>
+                                  <property name="iconSize">
+                                   <size>
+                                    <width>24</width>
+                                    <height>24</height>
+                                   </size>
+                                  </property>
+                                 </widget>
+                                </item>
+                               </layout>
+                              </item>
+                              <item>
+                               <widget class="QLabel" name="refuseLabel">
+                                <property name="layoutDirection">
+                                 <enum>Qt::RightToLeft</enum>
+                                </property>
+                                <property name="styleSheet">
+                                 <string notr="true">color: rgb(174, 174, 174);</string>
+                                </property>
+                                <property name="text">
+                                 <string>Ignore</string>
+                                </property>
+                                <property name="alignment">
+                                 <set>Qt::AlignCenter</set>
+                                </property>
+                               </widget>
+                              </item>
+                             </layout>
+                            </item>
+                           </layout>
+                          </item>
+                         </layout>
+                        </item>
+                       </layout>
                       </widget>
                      </item>
-                     <item>
-                      <layout class="QHBoxLayout" name="imSendHBoxLayout">
+                    </layout>
+                   </widget>
+                  </widget>
+                  <widget class="QWidget" name="messagesWidget" native="true">
+                   <property name="enabled">
+                    <bool>true</bool>
+                   </property>
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="minimumSize">
+                    <size>
+                     <width>358</width>
+                     <height>0</height>
+                    </size>
+                   </property>
+                   <property name="maximumSize">
+                    <size>
+                     <width>16777215</width>
+                     <height>16777215</height>
+                    </size>
+                   </property>
+                   <layout class="QVBoxLayout" name="verticalLayout_8">
+                    <property name="spacing">
+                     <number>0</number>
+                    </property>
+                    <property name="leftMargin">
+                     <number>0</number>
+                    </property>
+                    <property name="topMargin">
+                     <number>0</number>
+                    </property>
+                    <property name="rightMargin">
+                     <number>0</number>
+                    </property>
+                    <property name="bottomMargin">
+                     <number>0</number>
+                    </property>
+                    <item>
+                     <widget class="QWidget" name="messagingHeaderWidget" native="true">
+                      <property name="sizePolicy">
+                       <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+                        <horstretch>0</horstretch>
+                        <verstretch>0</verstretch>
+                       </sizepolicy>
+                      </property>
+                      <layout class="QHBoxLayout" name="horizontalLayout_10">
+                       <property name="spacing">
+                        <number>6</number>
+                       </property>
                        <property name="leftMargin">
-                        <number>16</number>
+                        <number>10</number>
                        </property>
                        <property name="topMargin">
-                        <number>16</number>
+                        <number>6</number>
                        </property>
                        <property name="rightMargin">
-                        <number>6</number>
+                        <number>10</number>
                        </property>
                        <property name="bottomMargin">
-                        <number>16</number>
+                        <number>6</number>
                        </property>
                        <item>
-                        <widget class="QLineEdit" name="imMessageEdit">
+                        <widget class="QPushButton" name="imBackButton">
+                         <property name="sizePolicy">
+                          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+                           <horstretch>0</horstretch>
+                           <verstretch>0</verstretch>
+                          </sizepolicy>
+                         </property>
                          <property name="minimumSize">
+                          <size>
+                           <width>30</width>
+                           <height>30</height>
+                          </size>
+                         </property>
+                         <property name="maximumSize">
+                          <size>
+                           <width>30</width>
+                           <height>30</height>
+                          </size>
+                         </property>
+                         <property name="toolTip">
+                          <string>Back to homepage button</string>
+                         </property>
+                         <property name="text">
+                          <string/>
+                         </property>
+                         <property name="icon">
+                          <iconset resource="ressources.qrc">
+                           <normaloff>:/images/icons/ic_arrow_back_24px.svg</normaloff>:/images/icons/ic_arrow_back_24px.svg</iconset>
+                         </property>
+                         <property name="iconSize">
+                          <size>
+                           <width>18</width>
+                           <height>18</height>
+                          </size>
+                         </property>
+                         <property name="flat">
+                          <bool>false</bool>
+                         </property>
+                        </widget>
+                       </item>
+                       <item>
+                        <layout class="QVBoxLayout" name="verticalLayout_16">
+                         <property name="spacing">
+                          <number>0</number>
+                         </property>
+                         <property name="leftMargin">
+                          <number>0</number>
+                         </property>
+                         <property name="bottomMargin">
+                          <number>0</number>
+                         </property>
+                         <item>
+                          <widget class="QLabel" name="imNameLabel">
+                           <property name="sizePolicy">
+                            <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+                             <horstretch>0</horstretch>
+                             <verstretch>0</verstretch>
+                            </sizepolicy>
+                           </property>
+                           <property name="minimumSize">
+                            <size>
+                             <width>200</width>
+                             <height>0</height>
+                            </size>
+                           </property>
+                           <property name="font">
+                            <font>
+                             <pointsize>10</pointsize>
+                             <weight>75</weight>
+                             <bold>true</bold>
+                            </font>
+                           </property>
+                           <property name="styleSheet">
+                            <string notr="true">color: rgb(63,63,63);</string>
+                           </property>
+                           <property name="text">
+                            <string/>
+                           </property>
+                           <property name="alignment">
+                            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+                           </property>
+                           <property name="wordWrap">
+                            <bool>false</bool>
+                           </property>
+                          </widget>
+                         </item>
+                         <item>
+                          <widget class="QLabel" name="imIdLabel">
+                           <property name="sizePolicy">
+                            <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+                             <horstretch>0</horstretch>
+                             <verstretch>0</verstretch>
+                            </sizepolicy>
+                           </property>
+                           <property name="minimumSize">
+                            <size>
+                             <width>200</width>
+                             <height>0</height>
+                            </size>
+                           </property>
+                           <property name="font">
+                            <font>
+                             <pointsize>9</pointsize>
+                            </font>
+                           </property>
+                           <property name="styleSheet">
+                            <string notr="true">color: rgb(192,192,192);</string>
+                           </property>
+                           <property name="text">
+                            <string/>
+                           </property>
+                           <property name="alignment">
+                            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+                           </property>
+                          </widget>
+                         </item>
+                        </layout>
+                       </item>
+                       <item>
+                        <spacer name="verticalSpacer_6">
+                         <property name="orientation">
+                          <enum>Qt::Vertical</enum>
+                         </property>
+                         <property name="sizeType">
+                          <enum>QSizePolicy::Fixed</enum>
+                         </property>
+                         <property name="sizeHint" stdset="0">
                           <size>
                            <width>0</width>
+                           <height>40</height>
+                          </size>
+                         </property>
+                        </spacer>
+                       </item>
+                       <item>
+                        <spacer name="horizontalSpacer_13">
+                         <property name="orientation">
+                          <enum>Qt::Horizontal</enum>
+                         </property>
+                         <property name="sizeHint" stdset="0">
+                          <size>
+                           <width>40</width>
+                           <height>20</height>
+                          </size>
+                         </property>
+                        </spacer>
+                       </item>
+                       <item>
+                        <widget class="QPushButton" name="btnAudioCall">
+                         <property name="minimumSize">
+                          <size>
+                           <width>30</width>
                            <height>30</height>
                           </size>
                          </property>
-                         <property name="font">
-                          <font>
-                           <pointsize>10</pointsize>
-                          </font>
+                         <property name="maximumSize">
+                          <size>
+                           <width>30</width>
+                           <height>30</height>
+                          </size>
                          </property>
-                         <property name="inputMask">
+                         <property name="text">
                           <string/>
                          </property>
-                         <property name="placeholderText">
-                          <string>Type your message here</string>
+                         <property name="icon">
+                          <iconset resource="ressources.qrc">
+                           <normaloff>:/images/icons/ic_phone_24px.svg</normaloff>:/images/icons/ic_phone_24px.svg</iconset>
+                         </property>
+                         <property name="iconSize">
+                          <size>
+                           <width>18</width>
+                           <height>18</height>
+                          </size>
                          </property>
                         </widget>
                        </item>
                        <item>
-                        <widget class="QPushButton" name="sendIMButton">
+                        <widget class="QPushButton" name="btnVideoCall">
+                         <property name="minimumSize">
+                          <size>
+                           <width>30</width>
+                           <height>30</height>
+                          </size>
+                         </property>
+                         <property name="maximumSize">
+                          <size>
+                           <width>30</width>
+                           <height>30</height>
+                          </size>
+                         </property>
+                         <property name="text">
+                          <string/>
+                         </property>
+                         <property name="icon">
+                          <iconset resource="ressources.qrc">
+                           <normaloff>:/images/icons/ic_video_call_24px.svg</normaloff>:/images/icons/ic_video_call_24px.svg</iconset>
+                         </property>
+                         <property name="iconSize">
+                          <size>
+                           <width>18</width>
+                           <height>18</height>
+                          </size>
+                         </property>
+                        </widget>
+                       </item>
+                       <item>
+                        <widget class="QPushButton" name="sendContactRequestButton">
+                         <property name="sizePolicy">
+                          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+                           <horstretch>0</horstretch>
+                           <verstretch>0</verstretch>
+                          </sizepolicy>
+                         </property>
                          <property name="minimumSize">
                           <size>
                            <width>30</width>
@@ -1406,14 +1761,17 @@
                           </size>
                          </property>
                          <property name="toolTip">
-                          <string>send message</string>
+                          <string>Add to contacts</string>
+                         </property>
+                         <property name="styleSheet">
+                          <string notr="true"/>
                          </property>
                          <property name="text">
                           <string/>
                          </property>
                          <property name="icon">
                           <iconset resource="ressources.qrc">
-                           <normaloff>:/images/icons/ic_send_24px.svg</normaloff>:/images/icons/ic_send_24px.svg</iconset>
+                           <normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</iconset>
                          </property>
                          <property name="iconSize">
                           <size>
@@ -1424,560 +1782,81 @@
                         </widget>
                        </item>
                       </layout>
-                     </item>
-                    </layout>
-                   </widget>
-                  </item>
-                  <item>
-                   <spacer name="horizontalSpacer_2">
-                    <property name="orientation">
-                     <enum>Qt::Horizontal</enum>
-                    </property>
-                    <property name="sizeType">
-                     <enum>QSizePolicy::Minimum</enum>
-                    </property>
-                    <property name="sizeHint" stdset="0">
-                     <size>
-                      <width>0</width>
-                      <height>20</height>
-                     </size>
-                    </property>
-                   </spacer>
-                  </item>
-                 </layout>
-                </item>
-               </layout>
-              </widget>
-              <widget class="QWidget" name="callInvitePage">
-               <layout class="QVBoxLayout" name="verticalLayout_9">
-                <property name="spacing">
-                 <number>0</number>
-                </property>
-                <property name="leftMargin">
-                 <number>0</number>
-                </property>
-                <property name="topMargin">
-                 <number>0</number>
-                </property>
-                <property name="rightMargin">
-                 <number>0</number>
-                </property>
-                <property name="bottomMargin">
-                 <number>0</number>
-                </property>
-                <item alignment="Qt::AlignHCenter|Qt::AlignVCenter">
-                 <widget class="QWidget" name="callInvite" native="true">
-                  <property name="sizePolicy">
-                   <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
-                    <horstretch>0</horstretch>
-                    <verstretch>0</verstretch>
-                   </sizepolicy>
-                  </property>
-                  <property name="minimumSize">
-                   <size>
-                    <width>166</width>
-                    <height>0</height>
-                   </size>
-                  </property>
-                  <property name="baseSize">
-                   <size>
-                    <width>166</width>
-                    <height>0</height>
-                   </size>
-                  </property>
-                  <layout class="QVBoxLayout" name="verticalLayout_4">
-                   <property name="spacing">
-                    <number>0</number>
-                   </property>
-                   <property name="leftMargin">
-                    <number>0</number>
-                   </property>
-                   <property name="topMargin">
-                    <number>0</number>
-                   </property>
-                   <property name="rightMargin">
-                    <number>0</number>
-                   </property>
-                   <property name="bottomMargin">
-                    <number>10</number>
-                   </property>
-                   <item>
-                    <layout class="QVBoxLayout" name="verticalLayout_3">
-                     <property name="spacing">
-                      <number>0</number>
-                     </property>
-                     <item>
-                      <widget class="QLabel" name="callerPhoto">
-                       <property name="sizePolicy">
-                        <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                         <horstretch>0</horstretch>
-                         <verstretch>0</verstretch>
-                        </sizepolicy>
-                       </property>
-                       <property name="minimumSize">
-                        <size>
-                         <width>130</width>
-                         <height>130</height>
-                        </size>
-                       </property>
-                       <property name="baseSize">
-                        <size>
-                         <width>130</width>
-                         <height>130</height>
-                        </size>
-                       </property>
-                       <property name="text">
-                        <string/>
-                       </property>
-                       <property name="pixmap">
-                        <pixmap resource="ressources.qrc">:/images/default_avatar_overlay.svg</pixmap>
-                       </property>
-                       <property name="alignment">
-                        <set>Qt::AlignCenter</set>
-                       </property>
-                      </widget>
-                     </item>
-                     <item alignment="Qt::AlignHCenter">
-                      <widget class="QLabel" name="callerIdLabel">
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(174, 174, 174);</string>
-                       </property>
-                       <property name="text">
-                        <string>Call</string>
-                       </property>
-                      </widget>
-                     </item>
-                     <item alignment="Qt::AlignHCenter">
-                      <widget class="QLabel" name="callerBestIdLabel">
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(174, 174, 174); font-style: italic;</string>
-                       </property>
-                       <property name="text">
-                        <string>BestId</string>
-                       </property>
-                      </widget>
-                     </item>
-                     <item>
-                      <widget class="QLabel" name="wantToTalkLabel">
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(174, 174, 174);</string>
-                       </property>
-                       <property name="text">
-                        <string>Wants to talk to you!</string>
-                       </property>
-                       <property name="alignment">
-                        <set>Qt::AlignCenter</set>
-                       </property>
-                      </widget>
-                     </item>
-                    </layout>
-                   </item>
-                   <item>
-                    <layout class="QHBoxLayout" name="horizontalLayout">
-                     <property name="topMargin">
-                      <number>20</number>
-                     </property>
-                     <item>
-                      <widget class="QPushButton" name="acceptButton">
-                       <property name="sizePolicy">
-                        <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
-                         <horstretch>0</horstretch>
-                         <verstretch>0</verstretch>
-                        </sizepolicy>
-                       </property>
-                       <property name="minimumSize">
-                        <size>
-                         <width>56</width>
-                         <height>56</height>
-                        </size>
-                       </property>
-                       <property name="toolTip">
-                        <string>Answer incoming call button</string>
-                       </property>
-                       <property name="icon">
-                        <iconset resource="ressources.qrc">
-                         <normaloff>:/images/icons/ic_done_white_24dp.png</normaloff>:/images/icons/ic_done_white_24dp.png</iconset>
-                       </property>
-                       <property name="iconSize">
-                        <size>
-                         <width>24</width>
-                         <height>24</height>
-                        </size>
-                       </property>
-                      </widget>
-                     </item>
-                     <item>
-                      <spacer name="horizontalSpacer_4">
-                       <property name="orientation">
-                        <enum>Qt::Horizontal</enum>
-                       </property>
-                       <property name="sizeType">
-                        <enum>QSizePolicy::Fixed</enum>
-                       </property>
-                       <property name="sizeHint" stdset="0">
-                        <size>
-                         <width>40</width>
-                         <height>20</height>
-                        </size>
-                       </property>
-                      </spacer>
-                     </item>
-                     <item>
-                      <widget class="QPushButton" name="refuseButton">
-                       <property name="sizePolicy">
-                        <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
-                         <horstretch>0</horstretch>
-                         <verstretch>0</verstretch>
-                        </sizepolicy>
-                       </property>
-                       <property name="minimumSize">
-                        <size>
-                         <width>56</width>
-                         <height>56</height>
-                        </size>
-                       </property>
-                       <property name="toolTip">
-                        <string>Ignore incoming call button</string>
-                       </property>
-                       <property name="layoutDirection">
-                        <enum>Qt::RightToLeft</enum>
-                       </property>
-                       <property name="icon">
-                        <iconset resource="ressources.qrc">
-                         <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset>
-                       </property>
-                       <property name="iconSize">
-                        <size>
-                         <width>24</width>
-                         <height>24</height>
-                        </size>
-                       </property>
-                      </widget>
-                     </item>
-                    </layout>
-                   </item>
-                   <item>
-                    <layout class="QHBoxLayout" name="horizontalLayout_7">
-                     <item>
-                      <widget class="QLabel" name="acceptLabel">
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(174, 174, 174);</string>
-                       </property>
-                       <property name="text">
-                        <string>Answer</string>
-                       </property>
-                       <property name="alignment">
-                        <set>Qt::AlignCenter</set>
-                       </property>
-                      </widget>
-                     </item>
-                     <item>
-                      <spacer name="horizontalSpacer_5">
-                       <property name="orientation">
-                        <enum>Qt::Horizontal</enum>
-                       </property>
-                       <property name="sizeType">
-                        <enum>QSizePolicy::Fixed</enum>
-                       </property>
-                       <property name="sizeHint" stdset="0">
-                        <size>
-                         <width>40</width>
-                         <height>20</height>
-                        </size>
-                       </property>
-                      </spacer>
-                     </item>
-                     <item>
-                      <widget class="QLabel" name="refuseLabel">
-                       <property name="layoutDirection">
-                        <enum>Qt::RightToLeft</enum>
-                       </property>
-                       <property name="styleSheet">
-                        <string notr="true">color: rgb(174, 174, 174);</string>
-                       </property>
-                       <property name="text">
-                        <string>Ignore</string>
-                       </property>
-                       <property name="alignment">
-                        <set>Qt::AlignCenter</set>
-                       </property>
-                      </widget>
-                     </item>
-                    </layout>
-                   </item>
-                  </layout>
-                 </widget>
-                </item>
-               </layout>
-              </widget>
-              <widget class="QWidget" name="outboundCallPage">
-               <layout class="QVBoxLayout" name="verticalLayout_11">
-                <property name="spacing">
-                 <number>0</number>
-                </property>
-                <property name="leftMargin">
-                 <number>0</number>
-                </property>
-                <property name="topMargin">
-                 <number>0</number>
-                </property>
-                <property name="rightMargin">
-                 <number>0</number>
-                </property>
-                <property name="bottomMargin">
-                 <number>0</number>
-                </property>
-                <item alignment="Qt::AlignHCenter|Qt::AlignVCenter">
-                 <widget class="QWidget" name="outboundCall" native="true">
-                  <layout class="QVBoxLayout" name="spinnerLayout">
-                   <item>
-                    <widget class="QLabel" name="callingPhoto">
-                     <property name="sizePolicy">
-                      <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                       <horstretch>0</horstretch>
-                       <verstretch>0</verstretch>
-                      </sizepolicy>
-                     </property>
-                     <property name="minimumSize">
-                      <size>
-                       <width>130</width>
-                       <height>130</height>
-                      </size>
-                     </property>
-                     <property name="baseSize">
-                      <size>
-                       <width>130</width>
-                       <height>130</height>
-                      </size>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                     <property name="pixmap">
-                      <pixmap resource="ressources.qrc">:/images/default_avatar_overlay.svg</pixmap>
-                     </property>
-                     <property name="alignment">
-                      <set>Qt::AlignCenter</set>
-                     </property>
-                    </widget>
-                   </item>
-                   <item>
-                    <widget class="QLabel" name="outboundCallLabel">
-                     <property name="styleSheet">
-                      <string notr="true">color: rgb(174, 174, 174);</string>
-                     </property>
-                     <property name="text">
-                      <string>Calling</string>
-                     </property>
-                     <property name="alignment">
-                      <set>Qt::AlignCenter</set>
-                     </property>
-                    </widget>
-                   </item>
-                   <item alignment="Qt::AlignHCenter">
-                    <widget class="QLabel" name="spinnerLabel">
-                     <property name="sizePolicy">
-                      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
-                       <horstretch>0</horstretch>
-                       <verstretch>0</verstretch>
-                      </sizepolicy>
-                     </property>
-                     <property name="minimumSize">
-                      <size>
-                       <width>20</width>
-                       <height>0</height>
-                      </size>
-                     </property>
-                     <property name="text">
-                      <string/>
-                     </property>
-                    </widget>
-                   </item>
-                   <item alignment="Qt::AlignHCenter">
-                    <widget class="QPushButton" name="cancelButton">
-                     <property name="sizePolicy">
-                      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
-                       <horstretch>0</horstretch>
-                       <verstretch>0</verstretch>
-                      </sizepolicy>
-                     </property>
-                     <property name="minimumSize">
-                      <size>
-                       <width>56</width>
-                       <height>56</height>
-                      </size>
-                     </property>
-                     <property name="maximumSize">
-                      <size>
-                       <width>64</width>
-                       <height>64</height>
-                      </size>
-                     </property>
-                     <property name="baseSize">
-                      <size>
-                       <width>64</width>
-                       <height>64</height>
-                      </size>
-                     </property>
-                     <property name="toolTip">
-                      <string>Cancel outgoing call</string>
-                     </property>
-                     <property name="layoutDirection">
-                      <enum>Qt::LeftToRight</enum>
-                     </property>
-                     <property name="icon">
-                      <iconset resource="ressources.qrc">
-                       <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset>
-                     </property>
-                     <property name="iconSize">
-                      <size>
-                       <width>24</width>
-                       <height>24</height>
-                      </size>
-                     </property>
-                    </widget>
-                   </item>
-                   <item>
-                    <widget class="QLabel" name="cancelCallLabel">
-                     <property name="styleSheet">
-                      <string notr="true">color: rgb(174, 174, 174);</string>
-                     </property>
-                     <property name="text">
-                      <string>Cancel</string>
-                     </property>
-                     <property name="alignment">
-                      <set>Qt::AlignCenter</set>
-                     </property>
-                    </widget>
-                   </item>
-                  </layout>
-                 </widget>
-                </item>
-               </layout>
-              </widget>
-              <widget class="QWidget" name="videoPage">
-               <layout class="QVBoxLayout" name="verticalLayout_10">
-                <property name="spacing">
-                 <number>0</number>
-                </property>
-                <property name="leftMargin">
-                 <number>0</number>
-                </property>
-                <property name="topMargin">
-                 <number>0</number>
-                </property>
-                <property name="rightMargin">
-                 <number>0</number>
-                </property>
-                <property name="bottomMargin">
-                 <number>0</number>
-                </property>
-                <item>
-                 <widget class="QSplitter" name="splitter_2">
-                  <property name="orientation">
-                   <enum>Qt::Horizontal</enum>
-                  </property>
-                  <widget class="VideoView" name="videoWidget" native="true">
-                   <property name="sizePolicy">
-                    <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
-                     <horstretch>0</horstretch>
-                     <verstretch>0</verstretch>
-                    </sizepolicy>
-                   </property>
-                  </widget>
-                  <widget class="InstantMessagingWidget" name="instantMessagingWidget" native="true">
-                   <property name="sizePolicy">
-                    <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
-                     <horstretch>0</horstretch>
-                     <verstretch>0</verstretch>
-                    </sizepolicy>
-                   </property>
-                   <property name="minimumSize">
-                    <size>
-                     <width>0</width>
-                     <height>0</height>
-                    </size>
-                   </property>
+                     </widget>
+                    </item>
+                    <item>
+                     <layout class="QHBoxLayout" name="messagesHBoxLayout" stretch="0,0,0">
+                      <property name="spacing">
+                       <number>0</number>
+                      </property>
+                      <property name="leftMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="topMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="rightMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="bottomMargin">
+                       <number>0</number>
+                      </property>
+                      <item>
+                       <spacer name="horizontalSpacer_5">
+                        <property name="orientation">
+                         <enum>Qt::Horizontal</enum>
+                        </property>
+                        <property name="sizeType">
+                         <enum>QSizePolicy::Maximum</enum>
+                        </property>
+                        <property name="sizeHint" stdset="0">
+                         <size>
+                          <width>0</width>
+                          <height>20</height>
+                         </size>
+                        </property>
+                       </spacer>
+                      </item>
+                      <item>
+                       <widget class="MessageWebView" name="messageView">
+                        <property name="sizePolicy">
+                         <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="maximumSize">
+                         <size>
+                          <width>680</width>
+                          <height>16777215</height>
+                         </size>
+                        </property>
+                       </widget>
+                      </item>
+                      <item>
+                       <spacer name="horizontalSpacer_14">
+                        <property name="orientation">
+                         <enum>Qt::Horizontal</enum>
+                        </property>
+                        <property name="sizeType">
+                         <enum>QSizePolicy::Maximum</enum>
+                        </property>
+                        <property name="sizeHint" stdset="0">
+                         <size>
+                          <width>0</width>
+                          <height>20</height>
+                         </size>
+                        </property>
+                       </spacer>
+                      </item>
+                     </layout>
+                    </item>
+                   </layout>
                   </widget>
                  </widget>
                 </item>
                </layout>
               </widget>
-              <widget class="QWidget" name="contactRequestPage">
-               <layout class="QVBoxLayout" name="verticalLayout_8">
-                <property name="leftMargin">
-                 <number>5</number>
-                </property>
-                <property name="topMargin">
-                 <number>5</number>
-                </property>
-                <property name="rightMargin">
-                 <number>0</number>
-                </property>
-                <property name="bottomMargin">
-                 <number>0</number>
-                </property>
-                <item>
-                 <layout class="QHBoxLayout" name="horizontalLayout_2">
-                  <item>
-                   <widget class="QPushButton" name="pendingCRBackButton">
-                    <property name="minimumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="maximumSize">
-                     <size>
-                      <width>30</width>
-                      <height>30</height>
-                     </size>
-                    </property>
-                    <property name="toolTip">
-                     <string>Back to homepage button</string>
-                    </property>
-                    <property name="text">
-                     <string/>
-                    </property>
-                    <property name="icon">
-                     <iconset resource="ressources.qrc">
-                      <normaloff>:/images/icons/ic_arrow_back_white_24dp.png</normaloff>:/images/icons/ic_arrow_back_white_24dp.png</iconset>
-                    </property>
-                    <property name="iconSize">
-                     <size>
-                      <width>18</width>
-                      <height>18</height>
-                     </size>
-                    </property>
-                   </widget>
-                  </item>
-                  <item>
-                   <spacer name="horizontalSpacer_13">
-                    <property name="orientation">
-                     <enum>Qt::Horizontal</enum>
-                    </property>
-                    <property name="sizeHint" stdset="0">
-                     <size>
-                      <width>40</width>
-                      <height>20</height>
-                     </size>
-                    </property>
-                   </spacer>
-                  </item>
-                 </layout>
-                </item>
-                <item>
-                 <widget class="ContactRequestWidget" name="contactRequestWidget" native="true">
-                  <property name="sizePolicy">
-                   <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-                    <horstretch>0</horstretch>
-                    <verstretch>0</verstretch>
-                   </sizepolicy>
-                  </property>
-                 </widget>
-                </item>
-               </layout>
-              </widget>
              </widget>
             </item>
            </layout>
@@ -1993,10 +1872,9 @@
  </widget>
  <customwidgets>
   <customwidget>
-   <class>InstantMessagingWidget</class>
+   <class>QWebEngineView</class>
    <extends>QWidget</extends>
-   <header>instantmessagingwidget.h</header>
-   <container>1</container>
+   <header location="global">QtWebEngineWidgets/QWebEngineView</header>
   </customwidget>
   <customwidget>
    <class>VideoView</class>
@@ -2019,18 +1897,6 @@
    <extends>QTreeView</extends>
    <header>smartlistview.h</header>
   </customwidget>
-  <customwidget>
-   <class>SendContactRequestWidget</class>
-   <extends>QWidget</extends>
-   <header>sendcontactrequestwidget.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>ContactRequestWidget</class>
-   <extends>QWidget</extends>
-   <header>contactrequestwidget.h</header>
-   <container>1</container>
-  </customwidget>
   <customwidget>
    <class>ConversationsFilterWidget</class>
    <extends>QWidget</extends>
@@ -2043,9 +1909,9 @@
    <header>currentaccountcombobox.h</header>
   </customwidget>
   <customwidget>
-   <class>ConversationFilterButton</class>
-   <extends>QWidget</extends>
-   <header>conversationfilterbutton.h</header>
+   <class>MessageWebView</class>
+   <extends>QWebEngineView</extends>
+   <header>messagewebview.h</header>
   </customwidget>
  </customwidgets>
  <resources>
diff --git a/conversationitemdelegate.cpp b/conversationitemdelegate.cpp
index 7ddb014..9c3f1f4 100644
--- a/conversationitemdelegate.cpp
+++ b/conversationitemdelegate.cpp
@@ -27,11 +27,12 @@
 #include "smartlistmodel.h"
 #include "ringthemeutils.h"
 #include "utils.h"
+#include "lrcinstance.h"
 
 #include <ciso646>
 
-ConversationItemDelegate::ConversationItemDelegate(QObject* parent) :
-    QItemDelegate(parent)
+ConversationItemDelegate::ConversationItemDelegate(QObject* parent)
+    : QItemDelegate(parent)
 {
 }
 
@@ -42,7 +43,7 @@ ConversationItemDelegate::paint(QPainter* painter
                         ) const
 {
     QStyleOptionViewItem opt(option);
-    painter->setRenderHint(QPainter::Antialiasing);
+    painter->setRenderHint(QPainter::Antialiasing, true);
 
     // Not having focus removes dotted lines around the item
     if (opt.state & QStyle::State_HasFocus)
@@ -139,8 +140,10 @@ ConversationItemDelegate::paint(QPainter* painter
 
 QSize
 ConversationItemDelegate::sizeHint(const QStyleOptionViewItem& option,
-                            const QModelIndex& index) const
+                                   const QModelIndex& index) const
 {
+    Q_UNUSED(option);
+    Q_UNUSED(index);
     return QSize(0, cellHeight_);
 }
 
@@ -150,6 +153,7 @@ ConversationItemDelegate::paintRingConversationItem(QPainter* painter,
                                                     const QRect& rect,
                                                     const QModelIndex& index) const
 {
+    Q_UNUSED(option);
     QFont font(painter->font());
     font.setPointSize(fontSize_);
     QPen pen(painter->pen());
@@ -221,18 +225,37 @@ ConversationItemDelegate::paintRingConversationItem(QPainter* painter,
     // bottom-right: last interaction snippet
     QString interactionStr = index.data(static_cast<int>(SmartListModel::Role::LastInteraction)).value<QString>();
     if (!interactionStr.isNull()) {
-        // remove phone glyphs
-        interactionStr.replace(QChar(0xd83d), "");
-        interactionStr.replace(QChar(0xdd7d), "");
-        interactionStr.replace(QChar(0xdcde), "");
-
-        font.setItalic(false);
-        font.setBold(false);
-        pen.setColor(RingTheme::grey_);
-        painter->setPen(pen);
-        painter->setFont(font);
+        painter->save();
+        interactionStr = interactionStr.simplified();
+        auto type = Utils::toEnum<lrc::api::interaction::Type>(index
+            .data(static_cast<int>(SmartListModel::Role::LastInteractionType))
+            .value<int>());
+        if (type == lrc::api::interaction::Type::CALL ||
+            type == lrc::api::interaction::Type::CONTACT) {
+            font.setItalic(false);
+            font.setBold(false);
+            pen.setColor(RingTheme::grey_.darker(140));
+            painter->setPen(pen);
+            painter->setFont(font);
+            // strip emojis if it's a call/contact type message
+            VectorUInt emojiless;
+            for (auto unicode : interactionStr.toUcs4()) {
+                if (!(unicode >= 0x1F000 && unicode <= 0x1FFFF)) {
+                    emojiless.push_back(unicode);
+                }
+            }
+            interactionStr = QString::fromUcs4(&emojiless.at(0), emojiless.size());
+        } else {
+            QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji"));
+            emojiMsgFont.setItalic(false);
+            emojiMsgFont.setBold(false);
+            emojiMsgFont.setPointSize(fontSize_);
+            painter->setOpacity(0.7);
+            painter->setFont(emojiMsgFont);
+        }
         interactionStr = fontMetrics.elidedText(interactionStr, Qt::ElideRight, rectInfo2.width());
         painter->drawText(rectInfo2, Qt::AlignVCenter | Qt::AlignRight, interactionStr);
+        painter->restore();
     }
 }
 
@@ -268,7 +291,7 @@ ConversationItemDelegate::paintRingInviteConversationItem(QPainter* painter,
     QFontMetrics fontMetrics(font);
 
     // The name is displayed at the avatar's right
-    QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>();;
+    QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>();
     if (!nameStr.isNull()) {
         font.setItalic(false);
         font.setBold(true);
@@ -293,6 +316,11 @@ ConversationItemDelegate::paintRingInviteConversationItem(QPainter* painter,
 }
 
 void
-ConversationItemDelegate::paintSIPConversationItem(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
+ConversationItemDelegate::paintSIPConversationItem(QPainter* painter,
+                                                   const QStyleOptionViewItem& option,
+                                                   const QModelIndex& index) const
 {
+    Q_UNUSED(painter);
+    Q_UNUSED(option);
+    Q_UNUSED(index);
 }
diff --git a/conversationsfilterwidget.cpp b/conversationsfilterwidget.cpp
index 07abdf5..5a34e53 100644
--- a/conversationsfilterwidget.cpp
+++ b/conversationsfilterwidget.cpp
@@ -18,9 +18,32 @@
 
 #include "conversationsfilterwidget.h"
 
+#include <QPainter>
+#include <QDebug>
+
 #include "ringthemeutils.h"
+#include "lrcinstance.h"
 
-#include <QPainter>
+ConversationsFilterWidget::ConversationsFilterWidget(QWidget *parent)
+    : QWidget(parent)
+{
+}
+
+void ConversationsFilterWidget::resizeEvent(QResizeEvent * event)
+{
+    using namespace lrc::api::profile;
+    updateNotifier(Type::RING);
+    updateNotifier(Type::PENDING);
+}
+
+void
+ConversationsFilterWidget::updateNotifier(lrc::api::profile::Type typeFilter)
+{
+    using namespace lrc::api::profile;
+    handleNotifierOverlay((typeFilter == Type::RING) ? "btnConversations" : "btnInvites",
+                          (typeFilter == Type::RING) ? unreadMessagesNotifier_ : pendingInvitesNotifier_,
+                          typeFilter);
+}
 
 static inline const QRect
 getNotifierRect(const QRect& buttonRect)
@@ -32,8 +55,8 @@ getNotifierRect(const QRect& buttonRect)
 
 void
 ConversationsFilterWidget::handleNotifierOverlay(const QString& buttonName,
-                                                 SmartlistSelectorButtonNotifier*& notifier,
-                                                 lrc::api::profile::Type filter)
+    SmartlistSelectorButtonNotifier*& notifier,
+    lrc::api::profile::Type filter)
 {
     auto button = this->findChild<QPushButton*>(buttonName);
     if (!button) {
@@ -49,18 +72,4 @@ ConversationsFilterWidget::handleNotifierOverlay(const QString& buttonName,
         notifier->setGeometry(getNotifierRect(button->frameGeometry()));
         notifier->show();
     }
-}
-
-ConversationsFilterWidget::ConversationsFilterWidget(QWidget *parent)
-    : QWidget(parent)
-{
-}
-
-void ConversationsFilterWidget::paintEvent(QPaintEvent * event)
-{
-    QWidget::paintEvent(event);
-
-    using namespace lrc::api::profile;
-    handleNotifierOverlay("buttonConversations", unreadMessagesNotifier_, Type::RING);
-    handleNotifierOverlay("buttonInvites", pendingInvitesNotifier_, Type::PENDING);
-}
+}
\ No newline at end of file
diff --git a/conversationsfilterwidget.h b/conversationsfilterwidget.h
index 1ce9eef..35a788f 100644
--- a/conversationsfilterwidget.h
+++ b/conversationsfilterwidget.h
@@ -22,20 +22,21 @@
 
 #include <QWidget>
 
-class ConversationsFilterWidget : public QWidget
-{
-    Q_OBJECT
-
-public:
-    explicit ConversationsFilterWidget(QWidget *parent = 0);
-
-protected:
-    virtual void paintEvent(QPaintEvent *event);
-
-private:
+class ConversationsFilterWidget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit ConversationsFilterWidget(QWidget *parent = 0);
+
+protected:
+    void resizeEvent(QResizeEvent * event);
+
+private:
+    void updateNotifier(lrc::api::profile::Type typeFilter);
     void handleNotifierOverlay(const QString& buttonName,
                                SmartlistSelectorButtonNotifier*& notifier,
-                               lrc::api::profile::Type filter);
-    SmartlistSelectorButtonNotifier* unreadMessagesNotifier_{ nullptr };
-    SmartlistSelectorButtonNotifier* pendingInvitesNotifier_{ nullptr };
+                               lrc::api::profile::Type filter);
+    SmartlistSelectorButtonNotifier* unreadMessagesNotifier_{ nullptr };
+    SmartlistSelectorButtonNotifier* pendingInvitesNotifier_{ nullptr };
 };
\ No newline at end of file
diff --git a/currentaccountcombobox.cpp b/currentaccountcombobox.cpp
index e17323b..59f0b67 100644
--- a/currentaccountcombobox.cpp
+++ b/currentaccountcombobox.cpp
@@ -60,8 +60,10 @@ CurrentAccountComboBox::CurrentAccountComboBox(QWidget* parent)
             });
 
     gearPixmap_.load(":/images/icons/round-settings-24px.svg");
+    gearLabel_.setPixmap(gearPixmap_);
     gearLabel_.setParent(this);
     gearLabel_.setStyleSheet("background: transparent;");
+    setupSettingsButton();
 }
 
 CurrentAccountComboBox::~CurrentAccountComboBox()
@@ -74,19 +76,13 @@ CurrentAccountComboBox::paintEvent(QPaintEvent* e)
 {
     Q_UNUSED(e);
 
-    gearPoint_.setX(this->width() - gearSize_ - 4 * gearBorder_);
-    gearPoint_.setY(this->height() / 2 - gearLabel_.height() / 2 - 2 * gearBorder_);
-    gearLabel_.setGeometry(gearPoint_.x() - 3, gearPoint_.y(),
-                           gearSize_ + 2 * gearBorder_, gearSize_ + 2 * gearBorder_);
-    gearLabel_.setMargin(gearBorder_);
-
     QPoint p(12, 2);
     QPainter painter(this);
     painter.setRenderHints((QPainter::Antialiasing | QPainter::TextAntialiasing), true);
 
     QStyleOption opt;
     opt.init(this);
-    style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter);
 
     // create box in which to draw avatar and presence indicator
     QRect avatarRect(2, 2, cellHeight_, cellHeight_); // [screen awareness]
@@ -136,10 +132,21 @@ CurrentAccountComboBox::paintEvent(QPaintEvent* e)
         painter.setPen(Qt::lightGray);
         painter.drawText(comboBoxRect, (Qt::AlignBottom | Qt::AlignLeft), secondaryAccountID);
     }
+}
 
-    this->setEnabled(LRCInstance::accountModel().getAccountList().size() > 1);
+void CurrentAccountComboBox::resizeEvent(QResizeEvent * event)
+{
+    setupSettingsButton();
+}
 
-    gearLabel_.setPixmap(gearPixmap_);
+void
+CurrentAccountComboBox::setupSettingsButton()
+{
+    gearPoint_.setX(this->width() - gearSize_ - 4 * gearBorder_);
+    gearPoint_.setY(this->height() / 2 - gearLabel_.height() / 2 - 2 * gearBorder_);
+    gearLabel_.setGeometry(gearPoint_.x() - 3, gearPoint_.y(),
+        gearSize_ + 2 * gearBorder_, gearSize_ + 2 * gearBorder_);
+    gearLabel_.setMargin(gearBorder_);
 }
 
 // import account background account pixmap and scale pixmap to fit in label
@@ -191,7 +198,7 @@ CurrentAccountComboBox::mouseMoveEvent(QMouseEvent* mouseEvent)
 void
 CurrentAccountComboBox::showPopup()
 {
-    gearPixmap_.load("");
+    gearLabel_.hide();
     popupPresent = true;
     QComboBox::showPopup();
 }
@@ -199,7 +206,7 @@ CurrentAccountComboBox::showPopup()
 void
 CurrentAccountComboBox::hidePopup()
 {
-    gearPixmap_.load(":/images/icons/round-settings-24px.svg");
+    gearLabel_.show();
     popupPresent = false;
     QComboBox::hidePopup();
 
diff --git a/currentaccountcombobox.h b/currentaccountcombobox.h
index df0367a..586d911 100644
--- a/currentaccountcombobox.h
+++ b/currentaccountcombobox.h
@@ -34,20 +34,22 @@ class CurrentAccountComboBox : public QComboBox
 public:
     explicit CurrentAccountComboBox(QWidget* parent = nullptr);
     ~CurrentAccountComboBox();
-    void accountListUpdate();
-    void setCurrentIndex(int index);
+    void accountListUpdate();
+    void setCurrentIndex(int index);
 
-signals:
-    void settingsButtonClicked();
+signals:
+    void settingsButtonClicked();
 
-private:
+protected:
     void paintEvent(QPaintEvent* e);
-    void importLabelPhoto(int index);
+    void resizeEvent(QResizeEvent *event);
     void mousePressEvent(QMouseEvent* mouseEvent);
-
-    void mouseMoveEvent(QMouseEvent* event);
+    void mouseMoveEvent(QMouseEvent* event);
     void leaveEvent(QEvent * event);
 
+private:
+    void importLabelPhoto(int index);
+    void setupSettingsButton();
     void showPopup();
     void hidePopup();
 
diff --git a/fetch-deps.bat b/fetch-deps.bat
new file mode 100644
index 0000000..052af8b
--- /dev/null
+++ b/fetch-deps.bat
@@ -0,0 +1,27 @@
+@echo off
+setlocal EnableDelayedExpansion
+
+set cloneSubmodules=N
+if "%1" == "/c" (
+	set cloneSubmodules=Y
+)
+
+if not exist "winsparkle" (
+	git clone --depth=1 https://github.com/vslavik/winsparkle.git
+)
+
+if "%cloneSubmodules%" neq "N" (
+    cd winsparkle
+	git submodule init
+	git submodule update
+	cd ..
+)
+
+if not exist "qrencode-win32" (
+	git clone --depth=1 https://github.com/BlueDragon747/qrencode-win32.git
+)
+
+:cleanup
+endlocal
+@endlocal
+exit /B %ERRORLEVEL%
\ No newline at end of file
diff --git a/fetch-qrencodewin32.bat b/fetch-qrencodewin32.bat
deleted file mode 100644
index fb5f5eb..0000000
--- a/fetch-qrencodewin32.bat
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-setlocal EnableDelayedExpansion
-
-set SRC=%~dp0
-
-set MSBUILD_ARGS=/nologo /p:useenv=true /p:Configuration=Release-Lib /p:Platform=x64 /verbosity:normal /maxcpucount:%NUMBER_OF_PROCESSORS%
-
-@setlocal
-
-set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer"
-if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer"
-
-pushd %VSInstallerFolder%
-for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
-  set VSLATESTDIR=%%i
-)
-popd
-
-echo VS Installation folder: %VSLATESTDIR%
-
-if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" (
-    echo:
-    echo VSInstallDir not found or not installed correctly.
-    goto cleanup
-)
-
-if %PROCESSOR_ARCHITECTURE%==x86 (
-    set Comp_x86=x86 10.0.15063.0
-    set Comp_x64=x86_amd64 10.0.15063.0
-) else (
-    set Comp_x86=amd64_x86 10.0.15063.0
-    set Comp_x64=amd64 10.0.15063.0
-)
-
-set path=%path:"=%
-call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64%
-
-git clone --depth=1 https://github.com/BlueDragon747/qrencode-win32.git
-
-msbuild qrencode-win32/qrencode-win32/vc8/qrcodelib/qrcodelib.vcxproj %MSBUILD_ARGS%
-
-:cleanup
-endlocal
-@endlocal
-exit /B %ERRORLEVEL%
\ No newline at end of file
diff --git a/fetch-winsparkle.bat b/fetch-winsparkle.bat
deleted file mode 100644
index e50cc6d..0000000
--- a/fetch-winsparkle.bat
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-setlocal EnableDelayedExpansion
-
-set SRC=%~dp0
-
-set MSBUILD_ARGS=/nologo /p:useenv=true /p:Configuration=Release /p:Platform=x64 /verbosity:normal /maxcpucount:%NUMBER_OF_PROCESSORS%
-
-@setlocal
-
-set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer"
-if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer"
-
-pushd %VSInstallerFolder%
-for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
-  set VSLATESTDIR=%%i
-)
-popd
-
-echo VS Installation folder: %VSLATESTDIR%
-
-if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" (
-    echo:
-    echo VSInstallDir not found or not installed correctly.
-    goto cleanup
-)
-
-if %PROCESSOR_ARCHITECTURE%==x86 (
-    set Comp_x86=x86 10.0.15063.0
-    set Comp_x64=x86_amd64 10.0.15063.0
-) else (
-    set Comp_x86=amd64_x86 10.0.15063.0
-    set Comp_x64=amd64 10.0.15063.0
-)
-
-set path=%path:"=%
-call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64%
-
-git clone --depth=1 https://github.com/vslavik/winsparkle.git
-
-msbuild winsparkle/WinSparkle-2015.vcxproj %MSBUILD_ARGS%
-
-:cleanup
-endlocal
-@endlocal
-exit /B %ERRORLEVEL%
\ No newline at end of file
diff --git a/imdelegate.cpp b/imdelegate.cpp
deleted file mode 100644
index 83ca35b..0000000
--- a/imdelegate.cpp
+++ /dev/null
@@ -1,191 +0,0 @@
-/***************************************************************************
- * Copyright (C) 2015-2018 by Savoir-faire Linux                           *
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
- *                                                                         *
- * This program is free software; you can redistribute it and/or modify    *
- * it under the terms of the GNU General Public License as published by    *
- * the Free Software Foundation; either version 3 of the License, or       *
- * (at your option) any later version.                                     *
- *                                                                         *
- * This program is distributed in the hope that it will be useful,         *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- * GNU General Public License for more details.                            *
- *                                                                         *
- * You should have received a copy of the GNU General Public License       *
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
- **************************************************************************/
-
-#include "imdelegate.h"
-
-#include <QApplication>
-#include <QSettings>
-#include <QDateTime>
-
-#include "media/text.h"
-#include "media/textrecording.h"
-
-#include "ringthemeutils.h"
-#include "settingskey.h"
-#include "messagemodel.h"
-#include "utils.h"
-
-ImDelegate::ImDelegate(QObject *parent)
-    : QItemDelegate(parent)
-{
-}
-
-void
-ImDelegate::formatMsg(const QModelIndex& index, QString& msgString) const
-{
-    auto date = index.data(static_cast<int>(MessageModel::Role::InteractionDate)).value<QDateTime>();
-    auto now = QDateTime::currentDateTime();
-    QString dateString;
-    if (now.date() == date.date()) {
-        dateString = date.time().toString();
-    } else {
-        dateString = date.toString();
-    }
-    msgString = QString("%1<br><footer><i>%2</i></footer>").arg(msgString, dateString);
-}
-
-void
-ImDelegate::paint(QPainter* painter,
-                  const QStyleOptionViewItem& option,
-                  const QModelIndex& index) const
-{
-    if (!index.isValid()) {
-        return;
-    }
-
-    auto msg = index.data(static_cast<int>(MessageModel::Role::Body)).toString();
-    auto type = static_cast<lrc::api::interaction::Type>(index.data(static_cast<int>(MessageModel::Role::Type)).value<int>());
-    auto isOutgoing = index.data(static_cast<int>(MessageModel::Role::Direction)).value<bool>();
-    auto isGenerated = Utils::isInteractionGenerated(type);
-    auto dir = isGenerated ? Qt::AlignHCenter : (isOutgoing ? Qt::AlignRight : Qt::AlignLeft);
-
-    QStyleOptionViewItem opt = option;
-
-    painter->setRenderHint(QPainter::Antialiasing);
-
-    opt.font = fontMsg_;
-    painter->setFont(fontMsg_);
-
-    opt.text.clear();
-
-    formatMsg(index, msg);
-
-    QTextDocument document;
-    document.setDefaultStyleSheet(defaultStylesheet_);
-    document.setDefaultFont(fontMsg_);
-    document.setHtml(msg);
-    auto textOptions = QTextOption(Qt::AlignLeft);
-    textOptions.setWrapMode(QTextOption::WrapMode::WordWrap);
-    document.setDefaultTextOption(textOptions);
-
-    QRect textRect = getBoundingRect(dir, opt, document);
-    document.setTextWidth(textRect.width());
-
-    if (dir == Qt::AlignLeft) {
-        // avatar
-        opt.decorationSize = QSize(sizeImage_, sizeImage_);
-        opt.decorationPosition = QStyleOptionViewItem::Left;
-        opt.decorationAlignment = Qt::AlignCenter;
-        QRect rectAvatar(margin_ + opt.rect.left(),
-                         margin_ + opt.rect.top(),
-                         sizeImage_, sizeImage_);
-        drawDecoration(painter, opt, rectAvatar,
-                       QPixmap::fromImage(index.data(Qt::DecorationRole).value<QImage>())
-                       .scaled(sizeImage_, sizeImage_, Qt::KeepAspectRatio, Qt::SmoothTransformation));
-    } else {
-        opt.decorationSize = QSize();
-        opt.decorationPosition = QStyleOptionViewItem::Right;
-    }
-
-    // message bubble
-    QPainterPath path;
-    path.addRoundedRect(textRect, bubbleRadius_, bubbleRadius_);
-
-    if (dir == Qt::AlignRight) {
-        painter->fillPath(path, RingTheme::imGrey_);
-    } else if (dir == Qt::AlignHCenter) {
-        painter->fillPath(path, Qt::transparent);
-    } else {
-        painter->fillPath(path, RingTheme::imBlue_);
-    }
-
-    painter->save();
-
-    // text
-    painter->translate(textRect.topLeft());
-    document.drawContents(painter);
-    painter->restore();
-}
-
-QRect ImDelegate::getBoundingRect(const Qt::AlignmentFlag& dir,
-                                  const QStyleOptionViewItem &option,
-                                  QTextDocument& txtDoc) const
-{
-    QRect textRect;
-
-    if (dir == Qt::AlignLeft) {
-        txtDoc.setTextWidth(option.rect.width() - sizeImage_ - padding_);
-        textRect.setRect(option.rect.left() + sizeImage_ + padding_,
-                         option.rect.top() + padding_,
-                         txtDoc.idealWidth(),
-                         txtDoc.size().height());
-    } else if (dir == Qt::AlignHCenter) {
-        txtDoc.setTextWidth(option.rect.width() - padding_);
-        auto optCenter = option.rect.left() + option.rect.width() / 2;
-        textRect.setRect(optCenter - txtDoc.idealWidth() / 2,
-                         option.rect.top() + padding_,
-                         txtDoc.idealWidth(),
-                         txtDoc.size().height());
-    } else {
-        txtDoc.setTextWidth(option.rect.width() - padding_);
-        textRect.setRect(option.rect.right() - padding_ - txtDoc.idealWidth(),
-                         option.rect.top() + padding_,
-                         txtDoc.idealWidth(),
-                         txtDoc.size().height());
-    }
-    return textRect;
-}
-
-QSize
-ImDelegate::sizeHint(const QStyleOptionViewItem& option,
-                     const QModelIndex& index) const
-{
-    QStyleOptionViewItem opt = option;
-    opt.font = fontMsg_;
-
-    QString msg = index.data(static_cast<int>(media::TextRecording::Role::FormattedHtml)).toString();
-
-    auto isOutgoing = index.data(static_cast<int>(MessageModel::Role::Direction)).value<bool>();
-    auto isGenerated = Utils::isInteractionGenerated(
-        static_cast<lrc::api::interaction::Type>(index.data(static_cast<int>(MessageModel::Role::Type)).value<int>())
-    );
-    auto dir = isGenerated ? Qt::AlignHCenter : (isOutgoing ? Qt::AlignRight : Qt::AlignLeft);
-
-    formatMsg(index, msg);
-
-    QTextDocument document;
-    document.setDefaultFont(fontMsg_);
-    document.setHtml(msg);
-    auto textOptions = QTextOption(Qt::AlignLeft);
-    textOptions.setWrapMode(QTextOption::WrapMode::WordWrap);
-    document.setDefaultTextOption(textOptions);
-
-    QRect boundingRect = getBoundingRect(dir, opt, document);
-
-    QSize size(boundingRect.width() + 2 * margin_, boundingRect.height());
-
-    /* Keep the minimum height needed. */
-    if(size.height() < sizeImage_)
-        size.setHeight(sizeImage_);
-
-    size.setHeight(size.height() + 2 * margin_);
-
-    return size;
-}
-
diff --git a/imdelegate.h b/imdelegate.h
deleted file mode 100644
index b05eb4e..0000000
--- a/imdelegate.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/***************************************************************************
- * Copyright (C) 2015-2017 by Savoir-faire Linux                           *
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
- *                                                                         *
- * This program is free software; you can redistribute it and/or modify    *
- * it under the terms of the GNU General Public License as published by    *
- * the Free Software Foundation; either version 3 of the License, or       *
- * (at your option) any later version.                                     *
- *                                                                         *
- * This program is distributed in the hope that it will be useful,         *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- * GNU General Public License for more details.                            *
- *                                                                         *
- * You should have received a copy of the GNU General Public License       *
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
- **************************************************************************/
-
-#pragma once
-
-#include <QPainter>
-#include <QTextDocument>
-#include <QItemDelegate>
-
-class ImDelegate : public QItemDelegate
-{
-    Q_OBJECT
-public:
-    explicit ImDelegate(QObject *parent = 0);
-
-protected:
-    void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
-    QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const;
-private:
-    void formatMsg(const QModelIndex& index, QString& msg) const;
-    QRect getBoundingRect(const Qt::AlignmentFlag& dir, const QStyleOptionViewItem &option,
-        QTextDocument& txtDoc) const;
-
-    const QFont fontMsg_ = QFont("Arial", 10);
-    const QString defaultStylesheet_ = QString("body { color : black; } i { opacity: 100; font-size : 10px; text-align : right; }");
-    constexpr static int sizeImage_ = 38;
-    constexpr static int margin_ = 5;
-    constexpr static int padding_ = 5;
-    constexpr static int bubbleRadius_ = 12;
-};
-
diff --git a/instantmessagingwidget.cpp b/instantmessagingwidget.cpp
deleted file mode 100644
index add65b8..0000000
--- a/instantmessagingwidget.cpp
+++ /dev/null
@@ -1,154 +0,0 @@
-/***************************************************************************
- * Copyright (C) 2015-2018 by Savoir-faire Linux                           *
- * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>*
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
- *                                                                         *
- * This program is free software; you can redistribute it and/or modify    *
- * it under the terms of the GNU General Public License as published by    *
- * the Free Software Foundation; either version 3 of the License, or       *
- * (at your option) any later version.                                     *
- *                                                                         *
- * This program is distributed in the hope that it will be useful,         *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- * GNU General Public License for more details.                            *
- *                                                                         *
- * You should have received a copy of the GNU General Public License       *
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
- **************************************************************************/
-
-#include "instantmessagingwidget.h"
-#include "ui_instantmessagingwidget.h"
-
-#include <QApplication>
-#include <QClipboard>
-#include <QMenu>
-
-#include "media/text.h"
-#include "media/textrecording.h"
-
-#include "imdelegate.h"
-#include "globalsystemtray.h"
-#include "settingskey.h"
-#include "lrcinstance.h"
-#include "utils.h"
-
-InstantMessagingWidget::InstantMessagingWidget(QWidget *parent) :
-    QWidget(parent),
-    ui(new Ui::InstantMessagingWidget)
-{
-    ui->setupUi(this);
-
-    this->hide();
-
-    imDelegate_ = new ImDelegate();
-    ui->listMessageView->setItemDelegate(imDelegate_);
-    ui->listMessageView->setContextMenuPolicy(Qt::ActionsContextMenu);
-    auto copyAction = new QAction(tr("Copy"), this);
-    ui->listMessageView->addAction(copyAction);
-    connect(copyAction, &QAction::triggered, [=]() {
-        copyToClipboard();
-    });
-}
-
-InstantMessagingWidget::~InstantMessagingWidget()
-{
-    delete ui;
-    delete imDelegate_;
-}
-
-void
-InstantMessagingWidget::setupCallMessaging(const std::string& callId,
-                                           MessageModel *messageModel)
-{
-    ui->listMessageView->disconnect();
-    ui->messageEdit->disconnect();
-    if (messageModel == nullptr) {
-        return;
-    }
-
-    using namespace lrc::api;
-
-    ui->listMessageView->setModel(messageModel);
-    ui->listMessageView->scrollToBottom();
-    connect(ui->messageEdit, &QLineEdit::returnPressed,
-        [=]() {
-            auto msg = ui->messageEdit->text();
-            if (msg.trimmed().isEmpty()) {
-                return;
-            }
-            ui->messageEdit->clear();
-            try {
-                auto selectedConvUid = LRCInstance::getSelectedConvUid();
-                LRCInstance::getCurrentConversationModel()->sendMessage(selectedConvUid, msg.toStdString());
-            }
-            catch (...) {
-                qDebug() << "exception when sending message";
-            }
-            ui->listMessageView->scrollToBottom();
-        });
-
-    QObject::disconnect(newInteractionConnection_);
-    newInteractionConnection_ = QObject::connect(LRCInstance::getCurrentConversationModel(), &ConversationModel::newInteraction,
-        [this](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction) {
-            onIncomingMessage(convUid, interactionId, interaction);
-        });
-}
-
-void
-InstantMessagingWidget::keyPressEvent(QKeyEvent *event)
-{
-    if (event->matches(QKeySequence::Copy)) {
-        copyToClipboard();
-    }
-}
-
-void
-InstantMessagingWidget::showEvent(QShowEvent *event)
-{
-    Q_UNUSED(event)
-    ui->messageEdit->setFocus();
-}
-
-void
-InstantMessagingWidget::copyToClipboard()
-{
-    auto idx = ui->listMessageView->currentIndex();
-    if (idx.isValid()) {
-        auto text = ui->listMessageView->model()->data(idx);
-
-        QApplication::clipboard()->setText(text.value<QString>());
-    }
-}
-
-void
-InstantMessagingWidget::updateConversationView(const std::string & convUid)
-{
-    auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo();
-    auto currentConversationModel = currentAccountInfo.conversationModel.get();
-    currentConversationModel->clearUnreadInteractions(convUid);
-    auto currentConversation = Utils::getConversationFromUid(convUid, *currentConversationModel);
-    if (currentConversation == currentConversationModel->allFilteredConversations().end()) {
-        return;
-    }
-    messageModel_.reset(new MessageModel(*currentConversation, currentAccountInfo, this->parent()));
-    ui->listMessageView->setModel(messageModel_.get());
-    ui->listMessageView->scrollToBottom();
-    this->show();
-}
-
-void
-InstantMessagingWidget::on_sendButton_clicked()
-{
-    emit ui->messageEdit->returnPressed();
-}
-
-void
-InstantMessagingWidget::onIncomingMessage(const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction)
-{
-    if (!QApplication::activeWindow() && settings_.value(SettingsKey::enableNotifications).toBool()) {
-        GlobalSystemTray::instance().showMessage("Ring: Message Received", QString::fromStdString(interaction.body));
-        QApplication::alert(this, 5000);
-    }
-    updateConversationView(convUid);
-}
diff --git a/instantmessagingwidget.ui b/instantmessagingwidget.ui
deleted file mode 100644
index 9dd2bec..0000000
--- a/instantmessagingwidget.ui
+++ /dev/null
@@ -1,124 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>InstantMessagingWidget</class>
- <widget class="QWidget" name="InstantMessagingWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>400</width>
-    <height>300</height>
-   </rect>
-  </property>
-  <property name="sizePolicy">
-   <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
-    <horstretch>0</horstretch>
-    <verstretch>0</verstretch>
-   </sizepolicy>
-  </property>
-  <property name="windowTitle">
-   <string/>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <property name="leftMargin">
-    <number>0</number>
-   </property>
-   <property name="topMargin">
-    <number>0</number>
-   </property>
-   <property name="rightMargin">
-    <number>0</number>
-   </property>
-   <property name="bottomMargin">
-    <number>0</number>
-   </property>
-   <item>
-    <widget class="QListView" name="listMessageView">
-     <property name="toolTip">
-      <string>Message list</string>
-     </property>
-     <property name="frameShape">
-      <enum>QFrame::NoFrame</enum>
-     </property>
-     <property name="verticalScrollMode">
-      <enum>QAbstractItemView::ScrollPerPixel</enum>
-     </property>
-     <property name="horizontalScrollMode">
-      <enum>QAbstractItemView::ScrollPerPixel</enum>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <property name="leftMargin">
-      <number>6</number>
-     </property>
-     <property name="topMargin">
-      <number>6</number>
-     </property>
-     <property name="rightMargin">
-      <number>6</number>
-     </property>
-     <property name="bottomMargin">
-      <number>6</number>
-     </property>
-     <item>
-      <widget class="QLineEdit" name="messageEdit">
-       <property name="minimumSize">
-        <size>
-         <width>0</width>
-         <height>30</height>
-        </size>
-       </property>
-       <property name="toolTip">
-        <string>Message input</string>
-       </property>
-       <property name="placeholderText">
-        <string>Send text message...</string>
-       </property>
-       <property name="clearButtonEnabled">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="sendButton">
-       <property name="minimumSize">
-        <size>
-         <width>30</width>
-         <height>30</height>
-        </size>
-       </property>
-       <property name="maximumSize">
-        <size>
-         <width>30</width>
-         <height>30</height>
-        </size>
-       </property>
-       <property name="toolTip">
-        <string>Send message button</string>
-       </property>
-       <property name="text">
-        <string/>
-       </property>
-       <property name="icon">
-        <iconset resource="ressources.qrc">
-         <normaloff>:/images/icons/ic_send_white_24dp.png</normaloff>:/images/icons/ic_send_white_24dp.png</iconset>
-       </property>
-       <property name="iconSize">
-        <size>
-         <width>18</width>
-         <height>18</height>
-        </size>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-  </layout>
- </widget>
- <resources>
-  <include location="ressources.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/invitebuttonswidget.cpp b/invitebuttonswidget.cpp
index ee18f90..cbe2504 100644
--- a/invitebuttonswidget.cpp
+++ b/invitebuttonswidget.cpp
@@ -37,11 +37,10 @@ InviteButtonsWidget::InviteButtonsWidget(QWidget* parent) :
         [=]() {
             emit btnBlockInviteClicked();
         });
-
 }
 
 InviteButtonsWidget::~InviteButtonsWidget()
 {
     disconnect(this);
     delete ui;
-}
\ No newline at end of file
+}
diff --git a/invitebuttonswidget.h b/invitebuttonswidget.h
index 7e1919b..7f53a57 100644
--- a/invitebuttonswidget.h
+++ b/invitebuttonswidget.h
@@ -19,6 +19,7 @@
 #pragma once
 
 #include <QWidget>
+#include <QItemDelegate>
 
 namespace Ui {
 class InviteButtonsWidget;
diff --git a/linkify.js b/linkify.js
new file mode 100644
index 0000000..8da2f89
--- /dev/null
+++ b/linkify.js
@@ -0,0 +1,1271 @@
+/*
+ * 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/lrcinstance.h b/lrcinstance.h
index 517eaae..1377dd6 100644
--- a/lrcinstance.h
+++ b/lrcinstance.h
@@ -93,6 +93,14 @@ public:
         instance().selectedConvUid = convUid;
     };
 
+    static void reset(bool newInstance = false) {
+        if (newInstance) {
+            instance().lrc_.reset(new lrc::api::Lrc());
+        } else {
+            instance().lrc_.reset();
+        }
+    };
+
 private:
     std::unique_ptr<lrc::api::Lrc> lrc_;
 
diff --git a/main.cpp b/main.cpp
index 5e3d70f..469351c 100644
--- a/main.cpp
+++ b/main.cpp
@@ -17,6 +17,7 @@
  **************************************************************************/
 
 #include "mainwindow.h"
+
 #include <QApplication>
 #include <QFile>
 
@@ -35,11 +36,13 @@
 
 #include <ciso646>
 
+#include "utils.h"
+
 #ifdef Q_OS_WIN
 #include <windows.h>
 #endif
 
-#ifdef _MSC_VER
+#if defined _MSC_VER && !COMPILE_ONLY
 #include <gnutls/gnutls.h>
 #endif
 
@@ -88,7 +91,7 @@ main(int argc, char *argv[])
     auto startMinimized = false;
     QString uri = "";
 
-#ifdef _MSC_VER
+#if defined _MSC_VER && !COMPILE_ONLY
     gnutls_global_init();
 #endif
 
diff --git a/mainwindow.cpp b/mainwindow.cpp
index efaa475..176853c 100644
--- a/mainwindow.cpp
+++ b/mainwindow.cpp
@@ -38,6 +38,7 @@
 #include "callwidget.h"
 #include "utils.h"
 #include "wizarddialog.h"
+#include "version.h"
 
 MainWindow::MainWindow(QWidget* parent) :
     QMainWindow(parent),
@@ -102,7 +103,7 @@ MainWindow::MainWindow(QWidget* parent) :
     readSettingsFromRegistry();
 
     win_sparkle_set_appcast_url("http://dl.ring.cx/windows/winsparkle-ring.xml");
-    win_sparkle_set_app_details(L"Savoir-faire Linux", L"Ring", QString(NIGHTLY_VERSION).toStdWString().c_str());
+    win_sparkle_set_app_details(L"Savoir-faire Linux", L"Ring", QString(VERSION_STRING).toStdWString().c_str());
     win_sparkle_set_shutdown_request_callback([]() {QCoreApplication::exit();});
     win_sparkle_set_did_find_update_callback([]() {MainWindow::instance().showNormal();});
     win_sparkle_init();
@@ -243,6 +244,7 @@ MainWindow::closeEvent(QCloseEvent* event)
         settings.setValue(SettingsKey::geometry, saveGeometry());
         settings.setValue(SettingsKey::windowState, saveState());
     }
+    LRCInstance::reset();
     QMainWindow::closeEvent(event);
 }
 
diff --git a/messagemodel.cpp b/messagemodel.cpp
deleted file mode 100644
index 998c97f..0000000
--- a/messagemodel.cpp
+++ /dev/null
@@ -1,123 +0,0 @@
-/***************************************************************************
-* Copyright (C) 2017 by Savoir-faire Linux                                 *
-* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>           *
-*                                                                          *
-* This program is free software; you can redistribute it and/or modify     *
-* it under the terms of the GNU General Public License as published by     *
-* the Free Software Foundation; either version 3 of the License, or        *
-* (at your option) any later version.                                      *
-*                                                                          *
-* This program is distributed in the hope that it will be useful,          *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of           *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
-* GNU General Public License for more details.                             *
-*                                                                          *
-* You should have received a copy of the GNU General Public License        *
-* along with this program.  If not, see <http://www.gnu.org/licenses/>.    *
-***************************************************************************/
-
-#include "messagemodel.h"
-
-// Qt
-#include <QDateTime>
-
-// LRC
-#include "globalinstances.h"
-#include "api/contactmodel.h"
-#include "api/conversationmodel.h"
-
-// Client
-#include "pixbufmanipulator.h"
-#include "utils.h"
-
-MessageModel::MessageModel(const ConversationInfo& conv, const AccountInfo& acc, QObject *parent)
-    : QAbstractItemModel(parent),
-    conv_(conv),
-    acc_(acc)
-{
-}
-
-int MessageModel::rowCount(const QModelIndex &parent) const
-{
-    if (!parent.isValid()) {
-        return conv_.interactions.size();
-    }
-    return 0; // A valid QModelIndex returns 0 as no entry has sub-elements
-}
-
-int MessageModel::columnCount(const QModelIndex &parent) const
-{
-    Q_UNUSED(parent);
-    return 1;
-}
-
-QVariant MessageModel::data(const QModelIndex &index, int role) const
-{
-    if (!index.isValid() || conv_.interactions.size() == 0) {
-        return QVariant();
-    }
-
-    auto it = conv_.interactions.begin();
-    std::advance(it, index.row());
-    const auto& item = (*it).second;
-    switch (role) {
-    case Role::Body:
-        return QVariant(QString::fromStdString(item.body));
-    case Role::Picture:
-    case Qt::DecorationRole:
-        return GlobalInstances::pixmapManipulator().decorationRole(conv_, acc_);
-    case Role::DisplayName:
-    case Qt::DisplayRole:
-    {
-        auto& contact = acc_.contactModel->getContact(conv_.participants[0]);
-        return QVariant(QString::fromStdString(Utils::bestNameForContact(contact)));
-    }
-    case Role::Presence:
-    {
-        auto& contact = acc_.contactModel->getContact(conv_.participants[0]);
-        return QVariant(contact.isPresent);
-    }
-    case Role::InteractionDate:
-    {
-        auto& date = item.timestamp;
-        return QVariant(QDateTime::fromTime_t(date));
-    }
-    case Role::Status:
-        return QVariant::fromValue(static_cast<int>(item.status));
-    case Role::Direction:
-        return QVariant::fromValue(lrc::api::interaction::isOutgoing(item));
-    case Role::Type:
-        return QVariant::fromValue(static_cast<int>(item.type));
-    }
-    return QVariant();
-}
-
-QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const
-{
-    Q_UNUSED(parent);
-    if (column != 0) {
-        return QModelIndex();
-    }
-
-    if (row >= 0 && row < rowCount()) {
-        return createIndex(row, column);
-    }
-    return QModelIndex();
-}
-
-QModelIndex MessageModel::parent(const QModelIndex &child) const
-{
-    Q_UNUSED(child);
-    return QModelIndex();
-}
-
-Qt::ItemFlags MessageModel::flags(const QModelIndex &index) const
-{
-    auto flags = QAbstractItemModel::flags(index) | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable;
-    if (!index.isValid()) {
-        return QAbstractItemModel::flags(index);
-    } else {
-        flags &= ~(Qt::ItemIsSelectable);
-    }
-    return flags;
-}
diff --git a/messagemodel.h b/messagemodel.h
deleted file mode 100644
index b8411c3..0000000
--- a/messagemodel.h
+++ /dev/null
@@ -1,61 +0,0 @@
-/***************************************************************************
-* Copyright (C) 2017 by Savoir-faire Linux                                 *
-* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>           *
-*                                                                          *
-* This program is free software; you can redistribute it and/or modify     *
-* it under the terms of the GNU General Public License as published by     *
-* the Free Software Foundation; either version 3 of the License, or        *
-* (at your option) any later version.                                      *
-*                                                                          *
-* This program is distributed in the hope that it will be useful,          *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of           *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
-* GNU General Public License for more details.                             *
-*                                                                          *
-* You should have received a copy of the GNU General Public License        *
-* along with this program.  If not, see <http://www.gnu.org/licenses/>.    *
-***************************************************************************/
-#pragma once
-
-// Qt include
-#include <QAbstractItemModel>
-
-// LRC
-#include "api/account.h"
-#include "api/conversation.h"
-#include "api/contact.h"
-
-class MessageModel : public QAbstractItemModel
-{
-    Q_OBJECT
-public:
-    using AccountInfo = lrc::api::account::Info;
-    using ConversationInfo = lrc::api::conversation::Info;
-    using ContactInfo = lrc::api::contact::Info;
-
-    enum Role {
-        Body = Qt::UserRole + 1,
-        DisplayName,
-        Picture,
-        Presence,
-        Status,
-        DataTransferStatus,
-        InteractionDate,
-        Direction,
-        Type
-    };
-
-    explicit MessageModel(const ConversationInfo& conv, const AccountInfo& acc, QObject *parent = 0);
-
-    // QAbstractItemModel
-    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-    int columnCount(const QModelIndex &parent) const override;
-    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-    QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;
-    QModelIndex parent(const QModelIndex &child) const;
-    Qt::ItemFlags flags(const QModelIndex &index) const;
-
-private:
-    ConversationInfo conv_;
-    const AccountInfo& acc_;
-};
diff --git a/conversationfilterbutton.cpp b/messagewebpage.cpp
similarity index 52%
rename from conversationfilterbutton.cpp
rename to messagewebpage.cpp
index 7bba253..441fc27 100644
--- a/conversationfilterbutton.cpp
+++ b/messagewebpage.cpp
@@ -1,65 +1,42 @@
-/***************************************************************************
- * Copyright (C) 2018 by Savoir-faire Linux                                *
- * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>                      *
- *                                                                         *
- * This program is free software; you can redistribute it and/or modify    *
- * it under the terms of the GNU General Public License as published by    *
- * the Free Software Foundation; either version 3 of the License, or       *
- * (at your option) any later version.                                     *
- *                                                                         *
- * This program is distributed in the hope that it will be useful,         *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- * GNU General Public License for more details.                            *
- *                                                                         *
- * You should have received a copy of the GNU General Public License       *
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
- **************************************************************************/
-
-#include "conversationfilterbutton.h"
-#include <QPainter>
-
-ConversationFilterButton::ConversationFilterButton()
-{
-    setCheckable(false);
-    connect(this, &ConversationFilterButton::pressed, this, &ConversationFilterButton::setSelected);
-}
-
-ConversationFilterButton::ConversationFilterButton(QWidget* widget)
-{
-    Q_UNUSED(widget)
-    setCheckable(false);
-    connect(this, &ConversationFilterButton::pressed, this, &ConversationFilterButton::setSelected);
-}
-
-ConversationFilterButton::~ConversationFilterButton()
-{
-}
-
-void
-ConversationFilterButton::paintEvent(QPaintEvent * e)
-{
-    QPushButton::paintEvent(e);
-    QPainter painter(this);
-
-    if (selected_) {
-        this->setStyleSheet("background-color: rgb(250, 250, 250)");
-    } else {
-        this->setStyleSheet("background-color: rgb(237, 237, 237)");
-    }
-    painter.end();
-}
-
-void
-ConversationFilterButton::setSelected()
-{
-    selected_ = true;
-}
-
-void
-ConversationFilterButton::setUnselected()
-{
-    selected_ = false;
-    update();
-}
-
+/***************************************************************************
+ * Copyright (C) 2018 by Savoir-faire Linux                                *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#include <QDesktopServices>
+
+#include "messagewebpage.h"
+
+MessageWebPage::MessageWebPage(QWidget *parent)
+    : QWebEnginePage(parent)
+{
+}
+
+MessageWebPage::~MessageWebPage()
+{
+}
+
+bool
+MessageWebPage::acceptNavigationRequest(const QUrl & url, QWebEnginePage::NavigationType type, bool isMainFrame)
+{
+    qDebug() << "acceptNavigationRequest(" << url << "," << type << "," << isMainFrame << ")";
+
+    if (type == QWebEnginePage::NavigationTypeLinkClicked) {
+        QDesktopServices::openUrl(url);
+        return false;
+    }
+    return true;
+}
diff --git a/conversationfilterbutton.h b/messagewebpage.h
similarity index 68%
rename from conversationfilterbutton.h
rename to messagewebpage.h
index 4877227..e8c4cea 100644
--- a/conversationfilterbutton.h
+++ b/messagewebpage.h
@@ -1,45 +1,35 @@
-/***************************************************************************
- * Copyright (C) 2018 by Savoir-faire Linux                                *
- * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>                      *
- *                                                                         *
- * This program is free software; you can redistribute it and/or modify    *
- * it under the terms of the GNU General Public License as published by    *
- * the Free Software Foundation; either version 3 of the License, or       *
- * (at your option) any later version.                                     *
- *                                                                         *
- * This program is distributed in the hope that it will be useful,         *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- * GNU General Public License for more details.                            *
- *                                                                         *
- * You should have received a copy of the GNU General Public License       *
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
- **************************************************************************/
-
-#pragma once
-
-#include <QPushButton>
-
-namespace Ui{
-    class ConvesationFilterButton;
-}
-
-class ConversationFilterButton : public QPushButton
-{
-    Q_OBJECT
-    ConversationFilterButton(const ConversationFilterButton& cpy);
-public:
-    ConversationFilterButton();
-    ConversationFilterButton(QWidget * widget);
-    ~ConversationFilterButton();
-
-public slots:
-    void setSelected();
-    void setUnselected();
-
-
-private:
-    void paintEvent(QPaintEvent* e);
-    bool selected_ = false;
-};
-
+/***************************************************************************
+ * Copyright (C) 2018 by Savoir-faire Linux                                *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#pragma once
+
+#include <QtWebEngineWidgets/QWebEngineView>
+
+class MessageWebPage : public QWebEnginePage
+{
+    Q_OBJECT
+public:
+    explicit MessageWebPage(QWidget* parent = nullptr);
+    ~MessageWebPage();
+
+protected:
+    virtual bool acceptNavigationRequest(const QUrl &url,
+                                         QWebEnginePage::NavigationType type,
+                                         bool isMainFrame);
+
+};
\ No newline at end of file
diff --git a/messagewebview.cpp b/messagewebview.cpp
new file mode 100644
index 0000000..5262edd
--- /dev/null
+++ b/messagewebview.cpp
@@ -0,0 +1,348 @@
+/***************************************************************************
+ * Copyright (C) 2017-2018 by Savoir-faire Linux                           *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+ * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>            *
+ * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com>            *
+ * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>              *
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#include "messagewebview.h"
+
+#include <QScrollBar>
+#include <QMouseEvent>
+#include <QDebug>
+#include <QMenu>
+#include <QDesktopServices>
+#include <QFileDialog>
+#include <QWebEnginePage>
+#include <QWebEngineScript>
+#include <QWebEngineScriptCollection>
+#include <QWebEngineSettings>
+#include <QWebChannel>
+#include <QTimer>
+
+#include <ciso646>
+#include <fstream>
+
+#include "utils.h"
+#include "webchathelpers.h"
+#include "messagewebpage.h"
+#include "lrcinstance.h"
+
+MessageWebView::MessageWebView(QWidget *parent)
+    : QWebEngineView(parent)
+{
+    setPage(new MessageWebPage());
+
+    settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
+    settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
+    settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false);
+    settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
+
+    setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu);
+
+    jsBridge_ = new PrivateBridging(this);
+    webChannel_ = new QWebChannel(page());
+    webChannel_->registerObject(QStringLiteral("jsbridge"), jsBridge_);
+    page()->setWebChannel(webChannel_);
+
+    connect(this, &QWebEngineView::renderProcessTerminated,
+        [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) {
+            Q_UNUSED(statusCode);
+            QString status;
+            switch (termStatus) {
+            case QWebEnginePage::NormalTerminationStatus:
+                qDebug() << "MessageWebView - Render process normal exit";
+                break;
+            case QWebEnginePage::AbnormalTerminationStatus:
+                qDebug() << "MessageWebView - Render process abnormal exit";
+                break;
+            case QWebEnginePage::CrashedTerminationStatus:
+                qDebug() << "MessageWebView - Render process crashed";
+                break;
+            case QWebEnginePage::KilledTerminationStatus:
+                qDebug() << "MessageWebView - Render process killed";
+                break;
+            }
+        });
+}
+
+MessageWebView::~MessageWebView()
+{
+}
+
+void MessageWebView::buildView()
+{
+    auto html = Utils::QByteArrayFromFile(":/web/chatview.html");
+    page()->setHtml(html, QUrl(":/web/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);
+}
+
+void
+MessageWebView::insertStyleSheet(const QString &name, const QString &source)
+{
+    QWebEngineScript script;
+    auto simplifiedCSS = source.simplified().replace("'", "\"");
+    QString s = QString::fromLatin1("(function() {"\
+                                    "    var node = document.createElement('style');"\
+                                    "    node.id = '%1';"\
+                                    "    node.innerHTML = '%2';"\
+                                    "    document.head.appendChild(node);"\
+                                    "})()").arg(name).arg(simplifiedCSS);
+    page()->runJavaScript(s);
+
+    script.setName(name);
+    script.setSourceCode(s);
+    script.setInjectionPoint(QWebEngineScript::DocumentReady);
+    script.setRunsOnSubFrames(true);
+    script.setWorldId(QWebEngineScript::MainWorld);
+    page()->scripts().insert(script);
+}
+
+void
+MessageWebView::removeStyleSheet(const QString &name)
+{
+    QString s = QString::fromLatin1("(function() {"\
+                                    "    var element = document.getElementById('%1');"\
+                                    "    element.outerHTML = '';"\
+                                    "    delete element;"\
+                                    "})()").arg(name);
+
+    page()->runJavaScript(s);
+
+    QWebEngineScript script = page()->scripts().findScript(name);
+    page()->scripts().remove(script);
+}
+
+void MessageWebView::clear()
+{
+    QString s = QString::fromLatin1("clearMessages();");
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::setDisplayLinks(bool display)
+{
+    QString s = QString::fromLatin1("setDisplayLinks('%1');")
+        .arg(display ? "true" : "false");
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::printNewInteraction(lrc::api::ConversationModel& conversationModel,
+                                    uint64_t msgId,
+                                    const lrc::api::interaction::Info& interaction)
+{
+    auto interactionObject = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
+    QString s = QString::fromLatin1("addMessage(%1);")
+        .arg(interactionObject.constData());
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::updateInteraction(lrc::api::ConversationModel& conversationModel,
+                                  uint64_t msgId,
+                                  const lrc::api::interaction::Info& interaction)
+{
+    auto interactionObject = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
+    QString s = QString::fromLatin1("updateMessage(%1);")
+        .arg(interactionObject.constData());
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::removeInteraction(uint64_t interactionId)
+{
+    QString s = QString::fromLatin1("removeInteraction(%1);")
+        .arg(QString::number(interactionId));
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::printHistory(lrc::api::ConversationModel& conversationModel,
+                             const std::map<uint64_t,
+                             lrc::api::interaction::Info> interactions)
+{
+    auto interactionsStr = interactionsToJsonArrayObject(conversationModel, interactions).toUtf8();
+    QString s = QString::fromLatin1("printHistory(%1);")
+        .arg(interactionsStr.constData());
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::setSenderImage(const std::string& sender,
+                               const std::string& senderImage)
+{
+    QJsonObject setSenderImageObject = QJsonObject();
+    setSenderImageObject.insert("sender_contact_method", QJsonValue(QString(sender.c_str())));
+    setSenderImageObject.insert("sender_image", QJsonValue(QString(senderImage.c_str())));
+
+    auto setSenderImageObjectString = QString(QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact));
+    QString s = QString::fromLatin1("setSenderImage(%1);")
+        .arg(setSenderImageObjectString.toUtf8().constData());
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::setInvitation(bool show, const std::string& contactUri)
+{
+    QString s = QString::fromLatin1(show ? "showInvitation(\"%1\")" : "showInvitation()")
+        .arg(QString(contactUri.c_str()));
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+void
+MessageWebView::hideMessages()
+{
+    QString s = QString::fromLatin1("hideBody();");
+    page()->runJavaScript(s, QWebEngineScript::MainWorld);
+}
+
+// JS bridging incoming
+Q_INVOKABLE int
+PrivateBridging::deleteInteraction(const QString& arg)
+{
+    bool ok;
+    uint64_t interactionUid = arg.toULongLong(&ok);
+    if (ok) {
+        LRCInstance::getCurrentConversationModel()->clearInteractionFromConversation(
+            LRCInstance::getSelectedConvUid(),
+            interactionUid
+        );
+    } else {
+        qDebug() << "deleteInteraction - invalid arg" << arg;
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::retryInteraction(const QString& arg)
+{
+    bool ok;
+    uint64_t interactionUid = arg.toULongLong(&ok);
+    if (ok) {
+        LRCInstance::getCurrentConversationModel()->retryInteraction(
+            LRCInstance::getSelectedConvUid(),
+            interactionUid
+        );
+    } else {
+        qDebug() << "retryInteraction - invalid arg" << arg;
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::openFile(const QString& arg)
+{
+    QDesktopServices::openUrl(arg);
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::acceptFile(const QString& arg)
+{
+    try {
+        auto interactionUid = std::stoull(arg.toStdString());
+
+        lrc::api::datatransfer::Info info = {};
+        auto convUid = LRCInstance::getSelectedConvUid();
+        LRCInstance::getCurrentConversationModel()->getTransferInfo(interactionUid, info);
+
+        auto downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation).toStdString();
+        // get full path
+        std::string filename = downloadsFolder.c_str();
+        if (!filename.empty() && filename.back() != '/')
+            filename += "/";
+        auto wantedFilename = filename + info.displayName;
+        auto duplicate = 0;
+        while (std::ifstream(wantedFilename).good()) {
+            ++duplicate;
+            auto extensionIdx = info.displayName.find_last_of(".");
+            if (extensionIdx == std::string::npos)
+                wantedFilename = filename + info.displayName + " (" + std::to_string(duplicate) + ")";
+            else
+                wantedFilename = filename + info.displayName.substr(0, extensionIdx) + " (" + std::to_string(duplicate) + ")" + info.displayName.substr(extensionIdx);
+        }
+        LRCInstance::getCurrentConversationModel()->acceptTransfer(convUid, interactionUid, wantedFilename);
+    } catch (...) {
+        qDebug() << "JS bridging - exception during acceptFile: " << arg;
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::refuseFile(const QString& arg)
+{
+    try {
+        auto interactionUid = std::stoull(arg.toStdString());
+        auto convUid = LRCInstance::getSelectedConvUid();
+        LRCInstance::getCurrentConversationModel()->cancelTransfer(convUid, interactionUid);
+    } catch (...) {
+        qDebug() << "JS bridging - exception during refuseFile:" << arg;
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::sendMessage(const QString& arg)
+{
+    try {
+        auto convUid = LRCInstance::getSelectedConvUid();
+        LRCInstance::getCurrentConversationModel()->sendMessage(convUid, arg.toStdString());
+    } catch (...) {
+        qDebug() << "JS bridging - exception during sendMessage:" << arg;
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::sendFile()
+{
+    qDebug() << "JS bridging - MessageWebView::sendFile";
+    QString filePath = QFileDialog::getOpenFileName((QWidget*)this->parent(), tr("Choose File"), "", tr("Files (*)"));
+    QFileInfo fi(filePath);
+    QString fileName = fi.fileName();
+    try {
+        auto convUid = LRCInstance::getSelectedConvUid();
+        LRCInstance::getCurrentConversationModel()->sendFile(convUid, filePath.toStdString(), fileName.toStdString());
+    } catch (...) {
+        qDebug() << "JS bridging - exception during sendFile";
+    }
+    return 0;
+}
+
+Q_INVOKABLE int
+PrivateBridging::log(const QString& arg)
+{
+    qDebug() << "JS log: " << arg;
+    return 0;
+}
\ No newline at end of file
diff --git a/messagewebview.h b/messagewebview.h
new file mode 100644
index 0000000..72b15e7
--- /dev/null
+++ b/messagewebview.h
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * Copyright (C) 2018 by Savoir-faire Linux                                *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#pragma once
+
+#include <QDebug>
+#include <QWebEngineView>
+
+#include "api/conversationmodel.h"
+
+class PrivateBridging : public QObject
+{
+    Q_OBJECT
+public:
+    explicit PrivateBridging(QObject* parent = nullptr) : QObject(parent) {};
+    ~PrivateBridging() {};
+
+    // exposed to JS through QWebChannel
+    Q_INVOKABLE int deleteInteraction(const QString& arg);
+    Q_INVOKABLE int retryInteraction(const QString& arg);
+    Q_INVOKABLE int openFile(const QString& arg);
+    Q_INVOKABLE int acceptFile(const QString& arg);
+    Q_INVOKABLE int refuseFile(const QString& arg);
+    Q_INVOKABLE int sendMessage(const QString & arg);
+    Q_INVOKABLE int sendFile();
+    Q_INVOKABLE int log(const QString& arg);
+
+};
+
+class MessageWebView : public QWebEngineView
+{
+    Q_OBJECT
+public:
+    explicit MessageWebView(QWidget* parent = nullptr);
+    ~MessageWebView();
+
+    void buildView();
+
+    void insertStyleSheet(const QString &name, const QString &source);
+    void removeStyleSheet(const QString &name);
+
+    void clear();
+    void setDisplayLinks(bool display);
+    void printNewInteraction(lrc::api::ConversationModel& conversationModel,
+                             uint64_t msgId,
+                             const lrc::api::interaction::Info& interaction);
+    void updateInteraction(lrc::api::ConversationModel& conversationModel,
+                           uint64_t msgId,
+                           const lrc::api::interaction::Info& interaction);
+    void removeInteraction(uint64_t interactionId);
+    void printHistory(lrc::api::ConversationModel& conversationModel,
+                      const std::map<uint64_t,
+                      lrc::api::interaction::Info> interactions);
+    void setSenderImage(const std::string& sender,
+                        const std::string& senderImage);
+    void setInvitation(bool show, const std::string& contactUri);
+    void hideMessages();
+
+private slots:
+    void slotLoadFinished();
+
+private:
+    QWebChannel* webChannel_;
+    PrivateBridging* jsBridge_;
+
+};
diff --git a/ressources.qrc b/ressources.qrc
index 5cf0ca6..51b40be 100644
--- a/ressources.qrc
+++ b/ressources.qrc
@@ -1,55 +1,62 @@
 <RCC>
-    <qresource prefix="/">
-        <file>stylesheet.css</file>
-        <file>images/ring.png</file>
-        <file>images/logo-ring-standard-coul.png</file>
-        <file>images/icons/ic_arrow_back_white_24dp.png</file>
-        <file>images/icons/ic_mic_off_white_24dp.png</file>
-        <file>images/icons/ic_person_add_white_24dp.png</file>
-        <file>images/icons/ic_search_black_18dp_2x.png</file>
-        <file>images/icons/ic_content_copy_white_24dp.png</file>
-        <file>images/icons/ic_send_white_24dp.png</file>
-        <file>images/icons/ic_group_add_white_24dp.png</file>
-        <file>images/icons/ic_videocam_off_white_24dp.png</file>
-        <file>images/icons/ic_high_quality_white_24dp.png</file>
-        <file>images/icons/ic_pause_white_24dp.png</file>
-        <file>images/icons/ic_chat_white_24dp.png</file>
-        <file>images/icons/ic_done_white_24dp.png</file>
-        <file>images/icons/ic_close_white_24dp.png</file>
-        <file>images/icons/ic_call_transfer_white_24px.png</file>
-        <file>images/icons/ic_pause_white_100px.png</file>
-        <file>images/icons/ic_videocam_white.png</file>
-        <file>images/icons/ic_add_black_18dp_2x.png</file>
-        <file>images/icons/ic_delete_black_18dp_2x.png</file>
-        <file>images/icons/ic_arrow_drop_down_black_18dp_2x.png</file>
-        <file>images/icons/ic_arrow_drop_up_black_18dp_2x.png</file>
-        <file>images/icons/ic_check_white_18dp_2x.png</file>
-        <file>images/icons/ic_folder_black_18dp_2x.png</file>
-        <file>images/icons/ic_arrow_forward_white_48dp_2x.png</file>
-        <file>images/icons/ic_settings_white_48dp_2x.png</file>
-        <file>images/icons/ic_voicemail_white_24dp_2x.png</file>
-        <file>images/background-light.png</file>
-        <file>images/background-dark.png</file>
-        <file>images/icons/ic_arrow_drop_down_black_9dp_2x.png</file>
-        <file>images/icons/ic_arrow_drop_up_black_9dp_2x.png</file>
-        <file>images/icons/ic_arrow_tab_next_black_9dp_2x.png</file>
-        <file>images/icons/ic_arrow_tab_previous_black_9dp_2x.png</file>
-        <file>images/spikeMask.png</file>
-        <file>images/icons/ic_photo_camera_white_24dp_2x.png</file>
-        <file>images/qrcode.png</file>
-        <file>images/icons/ic_share_black_48dp_2x.png</file>
-        <file>images/loading.gif</file>
-        <file>images/FontAwesome.otf</file>
-        <file>images/icons/ic_chat_black_24dp_2x.png</file>
-        <file>images/icons/ic_person_add_black_24dp_2x.png</file>
-        <file>images/waiting.gif</file>
-        <file>images/default_avatar_overlay.svg</file>
-	<file>images/icons/ic_clear_24px.svg</file>
-	<file>images/icons/ic_block_24px.svg</file>
-	<file>images/icons/ic_phone_24px.svg</file>
-	<file>images/icons/ic_video_call_24px.svg</file>
-	<file>images/icons/ic_arrow_back_24px.svg</file>
-	<file>images/icons/ic_send_24px.svg</file>
-	<file>images/icons/round-settings-24px.svg</file>	
-    </qresource>
+  <qresource prefix="/">
+    <file>stylesheet.css</file>
+    <file>images/ring.png</file>
+    <file>images/logo-ring-standard-coul.png</file>
+    <file>images/icons/ic_arrow_back_white_24dp.png</file>
+    <file>images/icons/ic_mic_off_white_24dp.png</file>
+    <file>images/icons/ic_person_add_white_24dp.png</file>
+    <file>images/icons/ic_search_black_18dp_2x.png</file>
+    <file>images/icons/ic_content_copy_white_24dp.png</file>
+    <file>images/icons/ic_send_white_24dp.png</file>
+    <file>images/icons/ic_group_add_white_24dp.png</file>
+    <file>images/icons/ic_videocam_off_white_24dp.png</file>
+    <file>images/icons/ic_high_quality_white_24dp.png</file>
+    <file>images/icons/ic_pause_white_24dp.png</file>
+    <file>images/icons/ic_chat_white_24dp.png</file>
+    <file>images/icons/ic_done_white_24dp.png</file>
+    <file>images/icons/ic_close_white_24dp.png</file>
+    <file>images/icons/ic_call_transfer_white_24px.png</file>
+    <file>images/icons/ic_pause_white_100px.png</file>
+    <file>images/icons/ic_videocam_white.png</file>
+    <file>images/icons/ic_add_black_18dp_2x.png</file>
+    <file>images/icons/ic_delete_black_18dp_2x.png</file>
+    <file>images/icons/ic_arrow_drop_down_black_18dp_2x.png</file>
+    <file>images/icons/ic_arrow_drop_up_black_18dp_2x.png</file>
+    <file>images/icons/ic_check_white_18dp_2x.png</file>
+    <file>images/icons/ic_folder_black_18dp_2x.png</file>
+    <file>images/icons/ic_arrow_forward_white_48dp_2x.png</file>
+    <file>images/icons/ic_settings_white_48dp_2x.png</file>
+    <file>images/icons/ic_voicemail_white_24dp_2x.png</file>
+    <file>images/background-light.png</file>
+    <file>images/background-dark.png</file>
+    <file>images/icons/ic_arrow_drop_down_black_9dp_2x.png</file>
+    <file>images/icons/ic_arrow_drop_up_black_9dp_2x.png</file>
+    <file>images/icons/ic_arrow_tab_next_black_9dp_2x.png</file>
+    <file>images/icons/ic_arrow_tab_previous_black_9dp_2x.png</file>
+    <file>images/spikeMask.png</file>
+    <file>images/icons/ic_photo_camera_white_24dp_2x.png</file>
+    <file>images/qrcode.png</file>
+    <file>images/icons/ic_share_black_48dp_2x.png</file>
+    <file>images/loading.gif</file>
+    <file>images/FontAwesome.otf</file>
+    <file>images/icons/ic_chat_black_24dp_2x.png</file>
+    <file>images/icons/ic_person_add_black_24dp_2x.png</file>
+    <file>images/waiting.gif</file>
+    <file>images/default_avatar_overlay.svg</file>
+    <file>images/icons/ic_clear_24px.svg</file>
+    <file>images/icons/ic_block_24px.svg</file>
+    <file>images/icons/ic_phone_24px.svg</file>
+    <file>images/icons/ic_video_call_24px.svg</file>
+    <file>images/icons/ic_arrow_back_24px.svg</file>
+    <file>images/icons/ic_send_24px.svg</file>
+    <file>images/icons/round-settings-24px.svg</file>
+    <file>web/chatview.css</file>
+    <file>web/chatview.html</file>
+    <file>web/chatview.js</file>
+    <file>web/linkify-html.js</file>
+    <file>web/linkify-string.js</file>
+    <file>web/linkify.js</file>
+    <file>web/qwebchannel.js</file>
+  </qresource>
 </RCC>
diff --git a/ring-client-windows.sln b/ring-client-windows.sln
index 0eb69bd..9106f8c 100644
--- a/ring-client-windows.sln
+++ b/ring-client-windows.sln
@@ -5,14 +5,14 @@ VisualStudioVersion = 15.0.27703.2026
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ring-client-windows", "ring-client-windows.vcxproj", "{0F6318E4-4C06-384E-BCA8-F344DA187957}"
 	ProjectSection(ProjectDependencies) = postProject
-		{103832FC-21E8-3D6C-8C85-21D927092562} = {103832FC-21E8-3D6C-8C85-21D927092562}
+		{A604BA33-C1DB-34F6-8584-C429857717A8} = {A604BA33-C1DB-34F6-8584-C429857717A8}
 	EndProjectSection
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ring-daemon", "..\daemon\MSVC\ring-daemon.vcxproj", "{79F8DE42-595D-49D9-A66F-55244FD9DCC3}"
 EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ringclient_static", "..\lrc\msvc\ringclient_static.vcxproj", "{103832FC-21E8-3D6C-8C85-21D927092562}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ringclient_static", "..\lrc\msvc\ringclient_static.vcxproj", "{A604BA33-C1DB-34F6-8584-C429857717A8}"
 EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qtwrapper", "..\lrc\msvc\src\qtwrapper\qtwrapper.vcxproj", "{75CF859A-E546-32BB-8806-448BDB78A8C4}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qtwrapper", "..\lrc\msvc\src\qtwrapper\qtwrapper.vcxproj", "{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}"
 	ProjectSection(ProjectDependencies) = postProject
 		{79F8DE42-595D-49D9-A66F-55244FD9DCC3} = {79F8DE42-595D-49D9-A66F-55244FD9DCC3}
 	EndProjectSection
@@ -99,60 +99,60 @@ Global
 		{79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x64.Build.0 = ReleaseLib|x64
 		{79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x86.ActiveCfg = ReleaseLib|Win32
 		{79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x86.Build.0 = ReleaseLib|Win32
-		{103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x64.ActiveCfg = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x64.Build.0 = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x86.ActiveCfg = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x64.ActiveCfg = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x64.Build.0 = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x64.ActiveCfg = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x64.Build.0 = Debug|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x86.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x86.Build.0 = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x64.Build.0 = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.Release|x64.ActiveCfg = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.Release|x86.ActiveCfg = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x64.ActiveCfg = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x64.Build.0 = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x64.ActiveCfg = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x64.Build.0 = Release|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x86.Build.0 = MinSizeRel|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64
-		{103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x64.ActiveCfg = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x64.Build.0 = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x86.ActiveCfg = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x64.ActiveCfg = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x64.Build.0 = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x64.ActiveCfg = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x64.Build.0 = Debug|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x86.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x86.Build.0 = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x64.Build.0 = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.Release|x64.ActiveCfg = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.Release|x86.ActiveCfg = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x64.ActiveCfg = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x64.Build.0 = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x64.ActiveCfg = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x64.Build.0 = Release|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x86.Build.0 = MinSizeRel|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64
-		{75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x64.ActiveCfg = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x64.Build.0 = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x86.ActiveCfg = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x64.ActiveCfg = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x64.Build.0 = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x64.ActiveCfg = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x64.Build.0 = Debug|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x86.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x86.Build.0 = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x64.Build.0 = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.Release|x64.ActiveCfg = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.Release|x86.ActiveCfg = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x64.ActiveCfg = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x64.Build.0 = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x64.ActiveCfg = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x64.Build.0 = Release|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x86.Build.0 = MinSizeRel|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64
+		{A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x64.ActiveCfg = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x64.Build.0 = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x86.ActiveCfg = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x64.ActiveCfg = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x64.Build.0 = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x64.ActiveCfg = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x64.Build.0 = Debug|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x86.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x86.Build.0 = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x64.Build.0 = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Release|x64.ActiveCfg = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Release|x86.ActiveCfg = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x64.ActiveCfg = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x64.Build.0 = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x64.ActiveCfg = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x64.Build.0 = Release|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x86.Build.0 = MinSizeRel|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64
+		{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/ring-client-windows.vcxproj b/ring-client-windows.vcxproj
index 7217d11..6019a7f 100644
--- a/ring-client-windows.vcxproj
+++ b/ring-client-windows.vcxproj
@@ -1,6 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="ReleaseCompile|x64">
+      <Configuration>ReleaseCompile</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
     <ProjectConfiguration Include="Release|x64">
       <Configuration>Release</Configuration>
       <Platform>x64</Platform>
@@ -22,6 +26,15 @@
     <IntermediateDirectory>release\</IntermediateDirectory>
     <PrimaryOutput>Ring</PrimaryOutput>
   </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'" Label="Configuration">
+    <PlatformToolset>v141</PlatformToolset>
+    <OutputDirectory>release\</OutputDirectory>
+    <ATLMinimizesCRunTimeLibraryUsage>false</ATLMinimizesCRunTimeLibraryUsage>
+    <CharacterSet>NotSet</CharacterSet>
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+    <IntermediateDirectory>release\</IntermediateDirectory>
+    <PrimaryOutput>Ring</PrimaryOutput>
+  </PropertyGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
   <PropertyGroup Condition="'$(QtMsBuild)'=='' or !Exists('$(QtMsBuild)\qt.targets')">
     <QtMsBuild>$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
@@ -36,17 +49,28 @@
   <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" />
   </ImportGroup>
+  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'" Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" />
+  </ImportGroup>
   <PropertyGroup Label="UserMacros" />
   <PropertyGroup>
     <OutDir Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(Platform)\$(Configuration)\</OutDir>
+    <OutDir Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(Platform)\$(Configuration)\</OutDir>
     <IntDir Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(Platform)\$(Configuration)\</IntDir>
+    <IntDir Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(Platform)\$(Configuration)\</IntDir>
     <TargetName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Ring</TargetName>
+    <TargetName Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">Ring</TargetName>
     <IgnoreImportLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</IgnoreImportLibrary>
+    <IgnoreImportLibrary Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">true</IgnoreImportLibrary>
     <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</LinkIncremental>
+    <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">false</LinkIncremental>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">
+    <TargetExt>.lib</TargetExt>
   </PropertyGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
     <ClCompile>
-      <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
       <AssemblerListingLocation>release\</AssemblerListingLocation>
       <BrowseInformation>false</BrowseInformation>
@@ -55,7 +79,7 @@
       <ExceptionHandling>Sync</ExceptionHandling>
       <ObjectFileName>$(IntDir)</ObjectFileName>
       <Optimization>MaxSpeed</Optimization>
-      <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <PreprocessToFile>false</PreprocessToFile>
       <ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName>
       <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
@@ -68,7 +92,82 @@
       <FunctionLevelLinking>true</FunctionLevelLinking>
     </ClCompile>
     <Link>
-      <AdditionalDependencies>..\daemon\MSVC\x64\ReleaseLib_win32\bin\dring.lib;..\lrc\msvc\src\qtwrapper\Release\qtwrapper.lib;..\lrc\msvc\release\ringclient_static.lib;.\winsparkle\x64\release\WinSparkle.lib;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib\qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;$(QTDIR)\lib\qtmain.lib;$(QTDIR)\lib\Qt5Svg.lib;$(QTDIR)\lib\Qt5Widgets.lib;$(QTDIR)\lib\Qt5WinExtras.lib;$(QTDIR)\lib\Qt5Gui.lib;$(QTDIR)\lib\Qt5Xml.lib;$(QTDIR)\lib\Qt5Network.lib;$(QTDIR)\lib\Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>dring.lib;qtwrapper.lib;ringclient_static.lib;WinSparkle.lib;qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;qtmain.lib;Qt5Svg.lib;Qt5Widgets.lib;Qt5WinExtras.lib;Qt5Gui.lib;Qt5Xml.lib;Qt5Network.lib;Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;$(QTDIR)\lib\Qt5WebEngineWidgets.lib;$(QTDIR)\lib\Qt5WebChannel.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalLibraryDirectories>$(QTDIR)\lib;..\ring-daemon\contrib\msvc\lib\x64;..\daemon\contrib\msvc\lib\x64;..\ring-daemon\MSVC\x64\ReleaseLib_win32\bin;..\daemon\MSVC\x64\ReleaseLib_win32\bin;..\ring-lrc\msvc\release;..\lrc\msvc\release;..\ring-lrc\msvc\src\qtwrapper\Release;..\lrc\msvc\src\qtwrapper\Release;.\winsparkle\x64\release;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+      <AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /ignore:4006,4049,4078,4098 /LTCG /NODEFAULTLIB:LIBCMT %(AdditionalOptions)</AdditionalOptions>
+      <DataExecutionPrevention>true</DataExecutionPrevention>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <IgnoreImportLibrary>true</IgnoreImportLibrary>
+      <LinkIncremental>false</LinkIncremental>
+      <OutputFile>$(OutDir)\Ring.exe</OutputFile>
+      <RandomizedBaseAddress>true</RandomizedBaseAddress>
+      <SubSystem>Windows</SubSystem>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <Version>2.0</Version>
+      <ForceFileOutput>MultiplyDefinedSymbolOnly</ForceFileOutput>
+    </Link>
+    <Midl>
+      <DefaultCharType>Unsigned</DefaultCharType>
+      <EnableErrorChecks>None</EnableErrorChecks>
+      <WarningLevel>0</WarningLevel>
+    </Midl>
+    <ResourceCompile>
+      <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ResourceCompile>
+    <QtMoc>
+      <QTDIR>$(QTDIR)</QTDIR>
+      <OutputFile>.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</OutputFile>
+      <Define>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define>
+      <CompilerFlavor>msvc</CompilerFlavor>
+      <Include>$(Configuration)/moc_predefs.h</Include>
+      <ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription>
+      <InputFile>%(FullPath)</InputFile>
+      <DynamicSource>output</DynamicSource>
+      <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</IncludePath>
+    </QtMoc>
+    <QtRcc>
+      <InitFuncName>ressources</InitFuncName>
+      <OutputFile>.\GeneratedFiles\qrc_%(Filename).cpp</OutputFile>
+      <QTDIR>$(QTDIR)</QTDIR>
+      <ExecutionDescription>Rcc'ing %(Identity)...</ExecutionDescription>
+      <Compression>default</Compression>
+      <InputFile>%(FullPath)</InputFile>
+    </QtRcc>
+    <QtUic>
+      <InputFile>%(FullPath)</InputFile>
+      <ExecutionDescription>Uic'ing %(Identity)...</ExecutionDescription>
+      <OutputFile>.\GeneratedFiles\ui_%(Filename).h</OutputFile>
+      <QTDIR>$(QTDIR)</QTDIR>
+    </QtUic>
+    <PreBuildEvent>
+      <Command>call touch_res.bat</Command>
+    </PreBuildEvent>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">
+    <ClCompile>
+      <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
+      <AssemblerListingLocation>release\</AssemblerListingLocation>
+      <BrowseInformation>false</BrowseInformation>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <DisableSpecificWarnings>4068;4099;4189;4267;4577;4467;4715;%(DisableSpecificWarnings)</DisableSpecificWarnings>
+      <ExceptionHandling>Sync</ExceptionHandling>
+      <ObjectFileName>$(IntDir)</ObjectFileName>
+      <Optimization>MaxSpeed</Optimization>
+      <PreprocessorDefinitions>COMPILE_ONLY;_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessToFile>false</PreprocessToFile>
+      <ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName>
+      <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType>
+      <WarningLevel>Level3</WarningLevel>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <SDLCheck>true</SDLCheck>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+    </ClCompile>
+    <Link>
+      <AdditionalDependencies>..\daemon\MSVC\x64\ReleaseLib_win32\bin\dring.lib;..\lrc\msvc\src\qtwrapper\Release\qtwrapper.lib;..\lrc\msvc\release\ringclient_static.lib;.\winsparkle\x64\release\WinSparkle.lib;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib\qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;$(QTDIR)\lib\qtmain.lib;$(QTDIR)\lib\Qt5Svg.lib;$(QTDIR)\lib\Qt5Widgets.lib;$(QTDIR)\lib\Qt5WinExtras.lib;$(QTDIR)\lib\Qt5Gui.lib;$(QTDIR)\lib\Qt5Xml.lib;$(QTDIR)\lib\Qt5Network.lib;$(QTDIR)\lib\Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;$(QTDIR)\lib\Qt5WebEngineWidgets.lib;$(QTDIR)\lib\Qt5WebChannel.lib;%(AdditionalDependencies)</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(QTDIR)\lib;..\daemon\contrib\msvc\lib\x64;..\daemon\MSVC\x64\ReleaseLib_win32\bin;..\lrc\msvc\release;.\winsparkle\x64\release;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /ignore:4006,4049,4078,4098 /LTCG /NODEFAULTLIB:LIBCMT %(AdditionalOptions)</AdditionalOptions>
       <DataExecutionPrevention>true</DataExecutionPrevention>
@@ -93,13 +192,13 @@
     <QtMoc>
       <QTDIR>$(QTDIR)</QTDIR>
       <OutputFile>.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</OutputFile>
-      <Define>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define>
+      <Define>COMPILE_ONLY;_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define>
       <CompilerFlavor>msvc</CompilerFlavor>
       <Include>$(Configuration)/moc_predefs.h</Include>
       <ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription>
       <InputFile>%(FullPath)</InputFile>
       <DynamicSource>output</DynamicSource>
-      <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)../daemon/contrib/msvc/include;$(ProjectDir)../lrc/src;$(ProjectDir)../client-windows/winsparkle/include;$(ProjectDir)../client-windows/qrencode-win32/qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;release;%(AdditionalIncludeDirectories)</IncludePath>
+      <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</IncludePath>
     </QtMoc>
     <QtRcc>
       <InitFuncName>ressources</InitFuncName>
@@ -115,9 +214,12 @@
       <OutputFile>.\GeneratedFiles\ui_%(Filename).h</OutputFile>
       <QTDIR>$(QTDIR)</QTDIR>
     </QtUic>
+    <PreBuildEvent>
+      <Command>call touch_res.bat</Command>
+    </PreBuildEvent>
   </ItemDefinitionGroup>
   <ItemGroup>
-    <ClCompile Include="conversationfilterbutton.cpp" />
+    <ClCompile Include="animationhelpers.cpp" />
     <ClCompile Include="currentaccountcombobox.cpp" />
     <ClCompile Include="aboutdialog.cpp" />
     <ClCompile Include="accountdetails.cpp" />
@@ -129,9 +231,13 @@
     <ClCompile Include="callutilsdialog.cpp" />
     <ClCompile Include="callwidget.cpp">
       <OutputFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile>
+      <OutputFile Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile>
       <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
+      <DynamicSource Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">input</DynamicSource>
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </ClCompile>
     <ClCompile Include="configurationwidget.cpp" />
     <ClCompile Include="contactpicker.cpp" />
@@ -139,12 +245,11 @@
     <ClCompile Include="deleteaccountdialog.cpp" />
     <ClCompile Include="globalsystemtray.cpp" />
     <ClCompile Include="idlabel.cpp" />
-    <ClCompile Include="imdelegate.cpp" />
-    <ClCompile Include="instantmessagingwidget.cpp" />
     <ClCompile Include="invitebuttonswidget.cpp" />
     <ClCompile Include="main.cpp" />
     <ClCompile Include="mainwindow.cpp" />
-    <ClCompile Include="messagemodel.cpp" />
+    <ClCompile Include="messagewebpage.cpp" />
+    <ClCompile Include="messagewebview.cpp" />
     <ClCompile Include="navwidget.cpp" />
     <ClCompile Include="photoboothdialog.cpp" />
     <ClCompile Include="photoboothwidget.cpp" />
@@ -156,9 +261,13 @@
     <ClCompile Include="sendcontactrequestwidget.cpp" />
     <ClCompile Include="conversationsfilterwidget.cpp">
       <OutputFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile>
+      <OutputFile Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile>
       <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
+      <DynamicSource Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">input</DynamicSource>
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </ClCompile>
     <ClCompile Include="smartlistselectorbuttonnotifier.cpp" />
     <ClCompile Include="smartlistview.cpp" />
@@ -168,6 +277,7 @@
     <ClCompile Include="videooverlay.cpp" />
     <ClCompile Include="videoview.cpp" />
     <ClCompile Include="videowidget.cpp" />
+    <ClCompile Include="webchathelpers.cpp" />
     <ClCompile Include="windowscontactbackend.cpp" />
     <ClCompile Include="wizarddialog.cpp" />
     <ClCompile Include="wizardwidget.cpp" />
@@ -175,7 +285,9 @@
   <ItemGroup>
     <QtMoc Include="wizardwidget.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <QtMoc Include="aboutdialog.h">
     </QtMoc>
@@ -198,11 +310,15 @@
     </QtMoc>
     <QtMoc Include="accountlistmodel.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
     </QtMoc>
     <QtMoc Include="accountitemdelegate.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
     </QtMoc>
     <QtMoc Include="contactrequestwidget.h">
     </QtMoc>
@@ -212,10 +328,6 @@
     </QtMoc>
     <QtMoc Include="idlabel.h">
     </QtMoc>
-    <QtMoc Include="imdelegate.h">
-    </QtMoc>
-    <QtMoc Include="instantmessagingwidget.h">
-    </QtMoc>
     <QtMoc Include="mainwindow.h">
     </QtMoc>
     <QtMoc Include="navwidget.h">
@@ -226,20 +338,34 @@
     </QtMoc>
     <QtMoc Include="invitebuttonswidget.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <QtMoc Include="currentaccountcombobox.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
-    <QtMoc Include="conversationfilterbutton.h">
-      <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+    <QtMoc Include="animationhelpers.h">
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <ClInclude Include="lrcinstance.h" />
-    <QtMoc Include="messagemodel.h">
-      <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
-      <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
+    <QtMoc Include="messagewebview.h">
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+    </QtMoc>
+    <QtMoc Include="messagewebpage.h">
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <ClInclude Include="pixbufmanipulator.h" />
     <QtMoc Include="qualitydialog.h">
@@ -256,23 +382,33 @@
     <ClInclude Include="settingskey.h" />
     <QtMoc Include="smartlistview.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
     </QtMoc>
     <QtMoc Include="conversationitemdelegate.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <QtMoc Include="smartlistmodel.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define>
     </QtMoc>
     <QtMoc Include="conversationsfilterwidget.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <QtMoc Include="smartlistselectorbuttonnotifier.h">
       <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
+      <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath>
       <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
+      <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define>
     </QtMoc>
     <ClInclude Include="utils.h" />
     <QtMoc Include="videooverlay.h">
@@ -281,6 +417,8 @@
     </QtMoc>
     <QtMoc Include="videowidget.h">
     </QtMoc>
+    <ClInclude Include="version.h" />
+    <ClInclude Include="webchathelpers.h" />
     <ClInclude Include="windowscontactbackend.h" />
     <QtMoc Include="wizarddialog.h">
     </QtMoc>
@@ -289,13 +427,18 @@
     <CustomBuild Include="debug\moc_predefs.h.cbt">
       <FileType>Document</FileType>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">true</ExcludedFromBuild>
     </CustomBuild>
     <CustomBuild Include="release\moc_predefs.h.cbt">
       <FileType>Document</FileType>
       <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs>
+      <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs>
       <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2&gt;NUL &gt;release\moc_predefs.h</Command>
+      <Command Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2&gt;NUL &gt;release\moc_predefs.h</Command>
       <Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Generate moc_predefs.h</Message>
+      <Message Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">Generate moc_predefs.h</Message>
       <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">release\moc_predefs.h;%(Outputs)</Outputs>
+      <Outputs Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">release\moc_predefs.h;%(Outputs)</Outputs>
     </CustomBuild>
     <ClInclude Include="ui_aboutdialog.h" />
     <ClInclude Include="ui_accountdetails.h" />
@@ -306,7 +449,6 @@
     <ClInclude Include="ui_contactpicker.h" />
     <ClInclude Include="ui_contactrequestwidget.h" />
     <ClInclude Include="ui_deleteaccountdialog.h" />
-    <ClInclude Include="ui_instantmessagingwidget.h" />
     <ClInclude Include="ui_mainwindow.h" />
     <ClInclude Include="ui_photoboothdialog.h" />
     <ClInclude Include="ui_photoboothwidget.h" />
@@ -371,12 +513,22 @@
     <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">
     </QtUic>
     <QtUic Include="accountdetails.ui">
     </QtUic>
+    <QtUic Include="animatedoverlay.ui">
+      <SubType>Designer</SubType>
+    </QtUic>
     <QtUic Include="bannedcontactswidget.ui">
     </QtUic>
     <QtUic Include="callutilsdialog.ui">
@@ -392,8 +544,6 @@
     </QtUic>
     <QtUic Include="deleteaccountdialog.ui">
     </QtUic>
-    <QtUic Include="instantmessagingwidget.ui">
-    </QtUic>
     <QtUic Include="invitebuttonswidget.ui" />
     <QtUic Include="mainwindow.ui">
       <SubType>Designer</SubType>
@@ -462,6 +612,7 @@
     <None Include="images\logo-ring-standard-coul.png" />
     <None Include="images\qrcode.png" />
     <QtRcc Include="ressources.qrc">
+      <SubType>Designer</SubType>
     </QtRcc>
     <None Include="images\ring.png" />
     <None Include="images\spikeMask.png" />
diff --git a/ring-client-windows.vcxproj.filters b/ring-client-windows.vcxproj.filters
index 951dfa2..d2e77d3 100644
--- a/ring-client-windows.vcxproj.filters
+++ b/ring-client-windows.vcxproj.filters
@@ -55,6 +55,9 @@
       <Extensions>ts;xlf</Extensions>
       <ParseFiles>false</ParseFiles>
     </Filter>
+    <Filter Include="Resource Files\web">
+      <UniqueIdentifier>{faf8f8a4-eb67-49df-a082-2fd64189e62c}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="aboutdialog.cpp">
@@ -93,12 +96,6 @@
     <ClCompile Include="idlabel.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
-    <ClCompile Include="imdelegate.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
-    <ClCompile Include="instantmessagingwidget.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
     <ClCompile Include="main.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
@@ -156,9 +153,6 @@
     <ClCompile Include="smartlistmodel.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
-    <ClCompile Include="messagemodel.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
     <ClCompile Include="accountlistmodel.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
@@ -186,7 +180,16 @@
     <ClCompile Include="currentaccountcombobox.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
-    <ClCompile Include="conversationfilterbutton.cpp">
+    <ClCompile Include="messagewebview.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="messagewebpage.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="webchathelpers.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="animationhelpers.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
   </ItemGroup>
@@ -230,12 +233,6 @@
     <QtMoc Include="idlabel.h">
       <Filter>Header Files</Filter>
     </QtMoc>
-    <QtMoc Include="imdelegate.h">
-      <Filter>Header Files</Filter>
-    </QtMoc>
-    <QtMoc Include="instantmessagingwidget.h">
-      <Filter>Header Files</Filter>
-    </QtMoc>
     <QtMoc Include="mainwindow.h">
       <Filter>Header Files</Filter>
     </QtMoc>
@@ -296,9 +293,6 @@
     <QtMoc Include="smartlistview.h">
       <Filter>Header Files</Filter>
     </QtMoc>
-    <QtMoc Include="messagemodel.h">
-      <Filter>Header Files</Filter>
-    </QtMoc>
     <QtMoc Include="accountlistmodel.h">
       <Filter>Header Files</Filter>
     </QtMoc>
@@ -323,7 +317,13 @@
     <QtMoc Include="currentaccountcombobox.h">
       <Filter>Header Files</Filter>
     </QtMoc>
-    <QtMoc Include="conversationfilterbutton.h">
+    <QtMoc Include="messagewebview.h">
+      <Filter>Header Files</Filter>
+    </QtMoc>
+    <QtMoc Include="messagewebpage.h">
+      <Filter>Header Files</Filter>
+    </QtMoc>
+    <QtMoc Include="animationhelpers.h">
       <Filter>Header Files</Filter>
     </QtMoc>
   </ItemGroup>
@@ -361,9 +361,6 @@
     <ClInclude Include="ui_deleteaccountdialog.h">
       <Filter>Generated Files</Filter>
     </ClInclude>
-    <ClInclude Include="ui_instantmessagingwidget.h">
-      <Filter>Generated Files</Filter>
-    </ClInclude>
     <ClInclude Include="ui_mainwindow.h">
       <Filter>Generated Files</Filter>
     </ClInclude>
@@ -552,6 +549,27 @@
     <None Include="translations\ring_client_windows_zh_TW.ts">
       <Filter>Translation Files</Filter>
     </None>
+    <None Include="web\chatview.css">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\chatview.html">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\linkify.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\linkify-html.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\linkify-string.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\chatview.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
+    <None Include="web\qwebchannel.js">
+      <Filter>Resource Files\web</Filter>
+    </None>
   </ItemGroup>
   <ItemGroup>
     <QtUic Include="aboutdialog.ui">
@@ -581,9 +599,6 @@
     <QtUic Include="deleteaccountdialog.ui">
       <Filter>Form Files</Filter>
     </QtUic>
-    <QtUic Include="instantmessagingwidget.ui">
-      <Filter>Form Files</Filter>
-    </QtUic>
     <QtUic Include="mainwindow.ui">
       <Filter>Form Files</Filter>
     </QtUic>
@@ -620,6 +635,9 @@
     <QtUic Include="wizardwidget.ui">
       <Filter>Form Files</Filter>
     </QtUic>
+    <QtUic Include="animatedoverlay.ui">
+      <Filter>Form Files</Filter>
+    </QtUic>
   </ItemGroup>
   <ItemGroup>
     <None Include="images\FontAwesome.otf">
@@ -765,5 +783,11 @@
     <ClInclude Include="lrcinstance.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="webchathelpers.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="version.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/ringthemeutils.h b/ringthemeutils.h
index 8b77e73..4536685 100644
--- a/ringthemeutils.h
+++ b/ringthemeutils.h
@@ -31,6 +31,7 @@ static const QColor red_ {251, 72, 71};
 static const QColor lightRed_ {252, 91, 90};
 static const QColor darkRed_ {219, 55, 54};
 static const QColor notificationRed_{ 255, 59, 48 };
+static const QColor urgentOrange_{ 255, 165, 0 };
 static const QColor green_ {127, 255, 0};
 static const QColor presenceGreen_{ 76, 217, 100 };
 static const QColor smartlistSelection_ { 237, 237, 237 };
diff --git a/settingskey.h b/settingskey.h
index 8a2453a..4290031 100644
--- a/settingskey.h
+++ b/settingskey.h
@@ -26,6 +26,7 @@ constexpr static char geometry[] = "geometry";
 constexpr static char windowState[] = "windowState";
 constexpr static char enableNotifications[] = "enableNotifications";
 constexpr static char selectedAccount[] = "selectedAccount";
+constexpr static char mainSplitterState[] = "mainSplitterState";
 }
 
 #define accountAutoAnswer(A) (A+SettingsKey::autoAnswer)
diff --git a/smartlistmodel.cpp b/smartlistmodel.cpp
index d767a03..be10454 100644
--- a/smartlistmodel.cpp
+++ b/smartlistmodel.cpp
@@ -29,7 +29,6 @@
 
 // Client
 #include "pixbufmanipulator.h"
-#include "messagemodel.h"
 #include "utils.h"
 
 SmartListModel::SmartListModel(const lrc::api::account::Info &acc, QObject *parent)
@@ -95,6 +94,8 @@ QVariant SmartListModel::data(const QModelIndex &index, int role) const
             }
             case Role::LastInteraction:
                 return QVariant(QString::fromStdString(item.interactions.at(item.lastMessageUid).body));
+            case Role::LastInteractionType:
+                return QVariant(Utils::toUnderlyingValue(item.interactions.at(item.lastMessageUid).type));
             case Role::ContactType:
             {
                 auto& contact = acc_.contactModel->getContact(item.participants[0]);
@@ -103,7 +104,7 @@ QVariant SmartListModel::data(const QModelIndex &index, int role) const
             case Role::UID:
                 return QVariant(QString::fromStdString(item.uid));
             case Role::ContextMenuOpen:
-                return QVariant(isContextMenuOpen_);
+                return QVariant(isContextMenuOpen);
             }
         } catch (...) {}
     }
diff --git a/smartlistmodel.h b/smartlistmodel.h
index 9071599..1da0a65 100644
--- a/smartlistmodel.h
+++ b/smartlistmodel.h
@@ -25,8 +25,6 @@
 #include "api/conversation.h"
 #include "api/contact.h"
 
-#include "messagemodel.h"
-
 namespace lrc { namespace api { class ConversationModel; } }
 
 class SmartListModel : public QAbstractItemModel
@@ -47,6 +45,7 @@ public:
         UnreadMessagesCount,
         LastInteractionDate,
         LastInteraction,
+        LastInteractionType,
         ContactType,
         UID,
         ContextMenuOpen
@@ -62,7 +61,8 @@ public:
     QModelIndex parent(const QModelIndex &child) const;
     Qt::ItemFlags flags(const QModelIndex &index) const;
 
-    bool isContextMenuOpen_{false};
+    // hack for context menu highlight retention
+    bool isContextMenuOpen{ false };
 
 private:
     const AccountInfo& acc_;
diff --git a/smartlistview.cpp b/smartlistview.cpp
index ae44423..4accf05 100644
--- a/smartlistview.cpp
+++ b/smartlistview.cpp
@@ -127,30 +127,18 @@ SmartListView::eventFilter(QObject* watched, QEvent* event)
     return QObject::eventFilter(watched, event);
 }
 
-void
-SmartListView::hideButtonsWidgets()
-{
-    auto model = this->model();
-    if (!model) {
-        return;
-    }
-    for (int i = 0; i < model->rowCount(); ++i) {
-        auto index = model->index(i, 0);
-        if (index.isValid() && indexWidget(index)) {
-            qDebug() << "hide a ButtonsWidgets";
-            indexWidget(index)->setVisible(false);
-        }
-    }
-}
-
 void
 SmartListView::drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
 {
     if (index == hoveredRow_ && indexWidget(hoveredRow_)) {
         indexWidget(index)->setVisible(true);
-    }
-    else if (indexWidget(index)) {
-        indexWidget(index)->setVisible(false);
+    } else if (indexWidget(index)) {
+        auto type = Utils::toEnum<lrc::api::profile::Type>(
+            index.data(static_cast<int>(SmartListModel::Role::ContactType)).value<int>()
+            );
+        if (type == lrc::api::profile::Type::PENDING) {
+            indexWidget(index)->setVisible(false);
+        }
     }
     QTreeView::drawRow(painter, option, index);
 }
\ No newline at end of file
diff --git a/smartlistview.h b/smartlistview.h
index 480109c..126c1a2 100644
--- a/smartlistview.h
+++ b/smartlistview.h
@@ -21,8 +21,6 @@
 
 #include <QTreeView>
 
-class ConversationItemDelegate;
-
 class SmartListView : public QTreeView
 {
     Q_OBJECT
@@ -38,7 +36,6 @@ protected:
     void drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
 
 private:
-    void hideButtonsWidgets();
     QModelIndex hoveredRow_;
 
 signals:
diff --git a/stylesheet.css b/stylesheet.css
index a1ef20a..63150a8 100644
--- a/stylesheet.css
+++ b/stylesheet.css
@@ -26,7 +26,7 @@ QPushButton#refuseButton:pressed, QPushButton#cancelButton:pressed{
     background-color: #db3c30;
 }
 
-QPushButton#buttonConversations, QPushButton#buttonInvites {
+QPushButton#btnConversations, QPushButton#btnInvites {
     background-color: rgb(242, 242, 242);
     border-style: solid;
     border-width: 0px;
@@ -35,14 +35,18 @@ QPushButton#buttonConversations, QPushButton#buttonInvites {
 	color: rgb(32, 32, 32);
 }
 
-QPushButton#buttonConversations:hover, QPushButton#buttonInvites:hover {
+QPushButton#btnConversations:hover, QPushButton#btnInvites:hover {
     background-color: rgb(237, 237, 237);
 }
 
-QPushButton#buttonConversations:pressed, QPushButton#buttonInvites:pressed {
+QPushButton#btnConversations:pressed, QPushButton#btnInvites:pressed {
     background-color: rgb(212, 212, 212);
 }
 
+QPushButton#btnConversations:checked, QPushButton#btnInvites:checked {
+    background-color: rgb(237, 237, 237);
+}
+
 QPushButton#imBackButton, QPushButton#btnAcceptInvite, QPushButton#btnIgnoreInvite,
 QPushButton#btnBlockInvite, QPushButton#btnAudioCall, QPushButton#btnVideoCall,
 QPushButton#sendContactRequestButton, QPushButton#sendButton, QPushButton#sendIMButton {
@@ -79,12 +83,6 @@ QWidget#callInvitePage, QWidget#outboundCallPage{
     background-position: bottom;
 }
 
-IdLabel{
-    border-style: solid;
-    border-width: 1px;
-    border-color: rgb(0, 192, 212);
-}
-
 RingContactLineEdit{
     border-color: rgb(242, 242, 242);
     border-radius: 5px;
@@ -163,9 +161,8 @@ QListView#contactRequestList::item:hover, QListView#BannedList::item:hover {
     background-color: rgba(242, 242, 242, 255);
 }
 
-QListView#listMessageView{
-    background: rgb(255, 255, 255);
-    border-bottom: 2px solid rgb(240, 240, 240);
+QWidget#sendIMWidget {
+	border-top: 2px solid rgb(240, 240, 240);
 }
 
 QWidget#messagingHeaderWidget{
@@ -175,6 +172,7 @@ QWidget#messagingHeaderWidget{
 QLineEdit#messageEdit, QLineEdit#imMessageEdit{
     border: none;
     background-color: rgb(255, 255, 255);
+	padding: 0px;
 }
 
 QLineEdit#numberBar{
@@ -248,21 +246,24 @@ QPushButton#videoCfgBtn:pressed{
 }
 
 QToolButton#qrButton, QToolButton#shareButton{
-    background-color: #3AC0D2;
-    border-radius: 15px;
-    border:solid 1px;
+    background-color: rgb(242, 242, 242);
+    border-style: solid;
+    border-width: 0px;
+    border-radius: 5px;
+    padding: 8px;
+	color: rgb(32, 32, 32);
 }
 
 QToolButton#qrButton:hover, QToolButton#shareButton:hover{
-    background-color: #4dc6d6;
+    background-color: rgb(237, 237, 237);
 }
 
 QToolButton#qrButton:pressed, QToolButton#shareButton:pressed{
-    background-color: #34acbd;
+    background-color: rgb(212, 212, 212);
 }
 
 QToolButton#qrButton:checked {
-    background-color: #34acbd;
+    background-color: rgb(237, 237, 237);
 }
 
 QPushButton#deleteAccountBtn, QToolButton#addAccountButton{
@@ -286,10 +287,7 @@ QScrollBar::handle:vertical{
 
 QDialog#WizardDialog, QWidget#welcomePage, QWidget#sendContactRequestPage,
 QDialog#DeleteAccountDialog, QDialog#DeleteContactDialog{
-    background: rgb(242, 242, 242);
-    background-image : url(:/images/background-light.png);
-    background-repeat : repeat-x;
-    background-position: bottom;
+    background: rgb(255, 255, 255);
 }
 
 QDialog#CallUtilsDialog, QDialog#QualityDialog{
diff --git a/touch_res.bat b/touch_res.bat
new file mode 100644
index 0000000..6b58825
--- /dev/null
+++ b/touch_res.bat
@@ -0,0 +1 @@
+copy ressources.qrc /B+ ,,/Y
\ No newline at end of file
diff --git a/utils.cpp b/utils.cpp
index 56ffb9c..870da11 100644
--- a/utils.cpp
+++ b/utils.cpp
@@ -36,6 +36,7 @@
 #include <QStackedWidget>
 #include <QPropertyAnimation>
 #include <QApplication>
+#include <QFile>
 
 #include "globalinstances.h"
 #include "pixbufmanipulator.h"
@@ -403,3 +404,15 @@ Utils::conversationPhoto(const std::string & convUid, const lrc::api::account::I
     QVariant var = GlobalInstances::pixmapManipulator().decorationRole(*conversation, accountInfo);
     return var.value<QImage>();
 }
+
+QByteArray
+Utils::QByteArrayFromFile(const QString& filename)
+{
+    QFile file(filename);
+    if (file.open(QIODevice::ReadOnly)) {
+        return file.readAll();
+    } else {
+        qDebug() << "can't open file";
+        return QByteArray();
+    }
+}
\ No newline at end of file
diff --git a/utils.h b/utils.h
index ffa2922..c13abde 100644
--- a/utils.h
+++ b/utils.h
@@ -37,6 +37,8 @@
 #include <QString>
 #include <QImage>
 #include <QStackedWidget>
+#include <QTextDocument>
+#include <QItemDelegate>
 
 #include "api/conversationmodel.h"
 #include "api/account.h"
@@ -74,8 +76,8 @@ namespace Utils
     bool isInteractionGenerated(const lrc::api::interaction::Type& interaction);
     bool isContactValid(const std::string& contactUid, const lrc::api::ConversationModel& model);
     QImage conversationPhoto(const std::string& convUid, const lrc::api::account::Info& accountInfo);
+    QByteArray QByteArrayFromFile(const QString& filename);
 
-    // helpers
     template<typename E>
     constexpr inline typename std::enable_if<   std::is_enum<E>::value,
         typename std::underlying_type<E>::type
@@ -104,4 +106,4 @@ namespace Utils
         }
         return std::distance(vec.begin(), it);
     }
-}
+}
\ No newline at end of file
diff --git a/version.h b/version.h
new file mode 100644
index 0000000..1ff8dd5
--- /dev/null
+++ b/version.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#define BUILD_YEAR_CH0 (__DATE__[ 7])
+#define BUILD_YEAR_CH1 (__DATE__[ 8])
+#define BUILD_YEAR_CH2 (__DATE__[ 9])
+#define BUILD_YEAR_CH3 (__DATE__[10])
+
+
+#define BUILD_MONTH_IS_JAN (__DATE__[0] == 'J' && __DATE__[1] == 'a' && __DATE__[2] == 'n')
+#define BUILD_MONTH_IS_FEB (__DATE__[0] == 'F')
+#define BUILD_MONTH_IS_MAR (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'r')
+#define BUILD_MONTH_IS_APR (__DATE__[0] == 'A' && __DATE__[1] == 'p')
+#define BUILD_MONTH_IS_MAY (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'y')
+#define BUILD_MONTH_IS_JUN (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'n')
+#define BUILD_MONTH_IS_JUL (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'l')
+#define BUILD_MONTH_IS_AUG (__DATE__[0] == 'A' && __DATE__[1] == 'u')
+#define BUILD_MONTH_IS_SEP (__DATE__[0] == 'S')
+#define BUILD_MONTH_IS_OCT (__DATE__[0] == 'O')
+#define BUILD_MONTH_IS_NOV (__DATE__[0] == 'N')
+#define BUILD_MONTH_IS_DEC (__DATE__[0] == 'D')
+
+
+#define BUILD_MONTH_CH0 \
+    ((BUILD_MONTH_IS_OCT || BUILD_MONTH_IS_NOV || BUILD_MONTH_IS_DEC) ? '1' : '0')
+
+#define BUILD_MONTH_CH1 \
+    ( \
+        (BUILD_MONTH_IS_JAN) ? '1' : \
+        (BUILD_MONTH_IS_FEB) ? '2' : \
+        (BUILD_MONTH_IS_MAR) ? '3' : \
+        (BUILD_MONTH_IS_APR) ? '4' : \
+        (BUILD_MONTH_IS_MAY) ? '5' : \
+        (BUILD_MONTH_IS_JUN) ? '6' : \
+        (BUILD_MONTH_IS_JUL) ? '7' : \
+        (BUILD_MONTH_IS_AUG) ? '8' : \
+        (BUILD_MONTH_IS_SEP) ? '9' : \
+        (BUILD_MONTH_IS_OCT) ? '0' : \
+        (BUILD_MONTH_IS_NOV) ? '1' : \
+        (BUILD_MONTH_IS_DEC) ? '2' : \
+        /* error default */    '?' \
+    )
+
+#define BUILD_DAY_CH0 ((__DATE__[4] >= '0') ? (__DATE__[4]) : '0')
+#define BUILD_DAY_CH1 (__DATE__[ 5])
+
+const char VERSION_STRING[] =
+{
+    BUILD_YEAR_CH0, BUILD_YEAR_CH1, BUILD_YEAR_CH2, BUILD_YEAR_CH3,
+    BUILD_MONTH_CH0, BUILD_MONTH_CH1,
+    BUILD_DAY_CH0, BUILD_DAY_CH1,
+    '\0'
+};
\ No newline at end of file
diff --git a/videoview.cpp b/videoview.cpp
index 5a9c70a..36c0a30 100644
--- a/videoview.cpp
+++ b/videoview.cpp
@@ -217,7 +217,8 @@ VideoView::dropEvent(QDropEvent* event)
 void
 VideoView::toggleFullScreen()
 {
-    overlay_->toggleContextButtons(isFullScreen());
+    qDebug() << "toggle FS";
+    /*overlay_->toggleContextButtons(isFullScreen());
     if(isFullScreen()) {
         dynamic_cast<QSplitter*>(oldParent_)->insertWidget(0,this);
         this->resize(oldSize_.width(), oldSize_.height());
@@ -228,7 +229,7 @@ VideoView::toggleFullScreen()
         this->setParent(0);
         this->showFullScreen();
     }
-    ui->videoWidget->setResetPreview(true);
+    ui->videoWidget->setResetPreview(true);*/
 }
 
 void
diff --git a/web/chatview.css b/web/chatview.css
new file mode 100644
index 0000000..5fa4156
--- /dev/null
+++ b/web/chatview.css
@@ -0,0 +1,921 @@
+/** Variable and font definitions */
+
+:root {
+    /* color definitions */
+    --ring-light-blue: rgba(59, 193, 211, 0.3);
+    /* main properties */
+    /* --bg-color: #f2f2f2; same as macOS client */
+    --bg-color: #ffffff; /* same as macOS client */
+    /* 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 ring 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);
+    /* 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: none;
+    opacity: 1;
+    transition: 0.5s opacity;
+}
+
+    body.fade {
+        opacity: 0;
+        transition: none;
+    }
+
+::-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: 9px;
+}
+
+    .nav-button.deactivated {
+        width: 30px;
+        height: 30px;
+        margin: 8px;
+        padding: 2px;
+        align-self: center;
+        display: flex;
+        border-radius: 9px;
+        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(--ring-light-blue);
+    position: absolute;
+    width: 100%;
+    /* hairline */
+    border-bottom: var(--hairline-thickness) solid var(--hairline-color);
+}
+
+    #invitation #actions {
+        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 #text h1 {
+        font-size: 1.5em;
+    }
+
+    #invitation #text {
+        text-align: center;
+        width: 90%;
+        margin: auto;
+        margin-top: 10px;
+        font-size: 0.8em;
+        /* enable selection (it is globally disabled in the body) */
+        -webkit-user-select: auto;
+    }
+
+.invitation-button,
+.flat-button {
+    margin: 5px;
+    border: 0;
+    border-radius: 5px;
+    transition: all 0.3s ease;
+    color: #f9f9f9;
+    padding: 10px 20px 10px 20px;
+    vertical-align: middle;
+    cursor: pointer;
+}
+
+.button-green {
+    background: #27ae60;
+}
+
+    .button-green:hover {
+        background: #1f8b4c;
+    }
+
+.button-red {
+    background: #dc3a37;
+}
+
+    .button-red:hover {
+        background: #b02e2c;
+    }
+
+/** 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;
+}
+
+#messages {
+    position: relative;
+    z-index: 0;
+    width: 100%;
+    overflow: hidden;
+    height: auto;
+    padding-top: 0.5em;
+}
+
+    #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;
+}
+
+/* General messages */
+
+.internal_mes_wrapper {
+    max-width: 70%;
+    margin: 8px 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 {
+    border-radius: 50%;
+    margin: 8px 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: 3px 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: 4px 0 0 0 !important;
+}
+
+@-webkit-keyframes fade-in {
+    from {
+        opacity: 0;
+    }
+
+    to {
+        opacity: 1;
+    }
+}
+
+.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 {
+    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;
+}
+
+.accept, .refuse {
+    border-radius: 50%;
+    cursor: pointer;
+}
+
+    .accept svg,
+    .refuse svg {
+        padding: 8px;
+        width: 24px;
+        height: 24px;
+    }
+
+.accept {
+    fill: #219d55;
+}
+
+    .accept:hover {
+        fill: white;
+        background: #219d55;
+    }
+
+.refuse {
+    fill: #dc2719;
+}
+
+    .refuse:hover {
+        fill: white;
+        background: #dc2719;
+    }
+
+.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: 5px 0 0 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: 200px;
+        max-height: 200px;
+    }
+}
+
+@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
new file mode 100644
index 0000000..43fbb18
--- /dev/null
+++ b/web/chatview.html
@@ -0,0 +1,43 @@
+<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="text">
+            </div>
+            <div id="actions">
+                <div id="accept-btn" class="invitation-button button-green" onclick="acceptInvitation()">Accept</div>
+                <div id="refuse-btn" class="invitation-button button-red" onclick="refuseInvitation()">Refuse</div>
+                <div id="block-btn" class="invitation-button button-red" onclick="blockConversation()">Block</div>
+            </div>
+        </div>
+    </div>
+    <div id="container">
+        <div id="messages" onscroll="onScrolled()"></div>
+        <div id="sendMessage">
+            <div class="nav-button action-button" onclick="sendFile()" 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
new file mode 100644
index 0000000..54994fb
--- /dev/null
+++ b/web/chatview.js
@@ -0,0 +1,1504 @@
+"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 invitationText    = document.getElementById("text")
+var   messages          = document.getElementById("messages")
+
+/* 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
+
+/* 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 (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
+ */
+/* exported showInvitation */
+function showInvitation(contactAlias) {
+    if (!contactAlias) {
+        if (hasInvitation) {
+            hasInvitation = false
+            invitation.style.visibility = ""
+        }
+    } else {
+        hasInvitation = true
+        invitationText.innerHTML = "<h1>" + contactAlias + " sends you an invitation</h1>"
+      + "Do you want to add them to the conversations list?<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 + " days ago"
+    }
+    if (interval === 1) {
+        return interval + " day ago"
+    }
+    interval = Math.floor(seconds / 3600)
+    if (interval > 1) {
+        return interval + " hours ago"
+    }
+    if (interval === 1) {
+        return interval + " hour ago"
+    }
+    interval = Math.floor(seconds / 60)
+    if (interval > 1) {
+        return interval + " minutes ago"
+    }
+    return "just now"
+}
+
+/**
+ * Send content of message bar
+ */
+function sendMessage()
+{
+    var message = messageBarInput.value
+    if (message.length > 0) {
+        messageBarInput.value = ""
+        window.jsbridge.sendMessage(message)
+    }
+}
+
+/* exported sendFile */
+function sendFile()
+{
+    window.jsbridge.sendFile();
+}
+
+/**
+ * Clear all messages.
+ */
+/* exported clearMessages */
+function clearMessages()
+{
+    while (messages.firstChild) {
+        messages.removeChild(messages.firstChild)
+    }
+}
+
+/**
+ * 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) {
+    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")
+    internal_mes_wrapper.appendChild(message_wrapper)
+
+    return internal_mes_wrapper
+}
+
+/**
+ * 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)
+            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_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
+        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, 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")
+    internal_mes_wrapper.appendChild(media_wrapper)
+
+    return internal_mes_wrapper
+}
+
+/**
+ * Build a new text interaction
+ * @param message_id
+ * @param htmlText the DOM to show
+ */
+function textInteraction(message_id, 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")
+    internal_mes_wrapper.appendChild(message_wrapper)
+
+    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(".internal_mes_wrapper").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.
+    // Add sender images if necessary (like if the interaction doesn't take the whole width)
+    const need_sender = (message_type === "data_transfer" || message_type === "text")
+    if (need_sender) {
+        var message_sender_image = document.createElement("span")
+        message_sender_image.setAttribute("class", `sender_image sender_image_${message_sender_contact_method}`)
+        message_div.appendChild(message_sender_image)
+    }
+
+    // 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)
+                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_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))
+        }
+    } 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_text, ytid))
+                }
+            }
+        }
+        if (type !== "media") {
+            type = "text"
+            message_div.append(textInteraction(message_id, 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)
+    }
+
+    return message_div
+}
+
+/**
+ * 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(".internal_mes_wrapper").appendChild(date_elt)
+        }
+
+        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 (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 hideBody()
+{
+    if (!document.body.classList.contains('fade')) {
+        document.body.classList.add('fade');
+    }
+}
+
+/**
+ * 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)
+{
+    hideBody()
+
+    historyBuffer = messages_array
+    historyBufferIndex = 0
+
+    isInitialLoading = true
+    printHistoryPart(messages, 0)
+    isInitialLoading = false
+
+    document.body.classList.remove('fade');
+}
+
+/**
+ * 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"],
+        sender_image = set_sender_image_object["sender_image"],
+        sender_image_id = "sender_image_" + sender_contact_method,
+        currentSenderImage = document.getElementById(sender_image_id), // Remove the currently set sender image
+        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: 32px;width: 32px;}"
+    document.head.appendChild(style)
+}
diff --git a/web/linkify-html.js b/web/linkify-html.js
new file mode 100644
index 0000000..1e5090c
--- /dev/null
+++ b/web/linkify-html.js
@@ -0,0 +1,827 @@
+/*
+ * 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
new file mode 100644
index 0000000..699a94d
--- /dev/null
+++ b/web/linkify-string.js
@@ -0,0 +1,118 @@
+/*
+ * 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
new file mode 100644
index 0000000..15dc03b
--- /dev/null
+++ b/web/linkify.js
@@ -0,0 +1,1271 @@
+/*
+ * 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
new file mode 100644
index 0000000..abf8461
--- /dev/null
+++ b/web/qwebchannel.js
@@ -0,0 +1,427 @@
+/****************************************************************************
+**
+** 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
+    };
+}
diff --git a/webchathelpers.cpp b/webchathelpers.cpp
new file mode 100644
index 0000000..9503d2d
--- /dev/null
+++ b/webchathelpers.cpp
@@ -0,0 +1,141 @@
+/***************************************************************************
+ * Copyright (C) 2017-2018 by Savoir-faire Linux                           *
+ * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>            *
+ * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com>            *
+ * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>              *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#include "webchathelpers.h"
+
+QJsonObject
+buildInteractionJson(lrc::api::ConversationModel& conversationModel,
+    const uint64_t msgId,
+    const lrc::api::interaction::Info& interaction)
+{
+    auto sender = QString(interaction.authorUri.c_str());
+    auto timestamp = QString::number(interaction.timestamp);
+    auto direction = lrc::api::interaction::isOutgoing(interaction) ? QString("out") : QString("in");
+
+    QJsonObject interactionObject = QJsonObject();
+    interactionObject.insert("text", QJsonValue(QString(interaction.body.c_str())));
+    interactionObject.insert("id", QJsonValue(QString::number(msgId)));
+    interactionObject.insert("sender", QJsonValue(sender));
+    interactionObject.insert("sender_contact_method", QJsonValue(sender));
+    interactionObject.insert("timestamp", QJsonValue(timestamp));
+    interactionObject.insert("direction", QJsonValue(direction));
+
+    switch (interaction.type)
+    {
+    case lrc::api::interaction::Type::TEXT:
+        interactionObject.insert("type", QJsonValue("text"));
+        break;
+    case lrc::api::interaction::Type::CALL:
+        interactionObject.insert("type", QJsonValue("call"));
+        break;
+    case lrc::api::interaction::Type::CONTACT:
+        interactionObject.insert("type", QJsonValue("contact"));
+        break;
+    case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
+    case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER: {
+        interactionObject.insert("type", QJsonValue("data_transfer"));
+        lrc::api::datatransfer::Info info = {};
+        conversationModel.getTransferInfo(msgId, info);
+        if (info.status != lrc::api::datatransfer::Status::INVALID) {
+            interactionObject.insert("totalSize", QJsonValue(qint64(info.totalSize)));
+            interactionObject.insert("progress", QJsonValue(qint64(info.progress)));
+        }
+        break;
+    }
+    case lrc::api::interaction::Type::INVALID:
+    default:
+        interactionObject.insert("type", QJsonValue(""));
+        break;
+    }
+
+    switch (interaction.status)
+    {
+    case lrc::api::interaction::Status::READ:
+        interactionObject.insert("delivery_status", QJsonValue("read"));
+        break;
+    case lrc::api::interaction::Status::SUCCEED:
+        interactionObject.insert("delivery_status", QJsonValue("sent"));
+        break;
+    case lrc::api::interaction::Status::FAILED:
+    case lrc::api::interaction::Status::TRANSFER_ERROR:
+        interactionObject.insert("delivery_status", QJsonValue("failure"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
+        interactionObject.insert("delivery_status", QJsonValue("unjoinable peer"));
+        break;
+    case lrc::api::interaction::Status::SENDING:
+        interactionObject.insert("delivery_status", QJsonValue("sending"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_CREATED:
+        interactionObject.insert("delivery_status", QJsonValue("connecting"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
+        interactionObject.insert("delivery_status", QJsonValue("accepted"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_CANCELED:
+        interactionObject.insert("delivery_status", QJsonValue("canceled"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_ONGOING:
+        interactionObject.insert("delivery_status", QJsonValue("ongoing"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
+        interactionObject.insert("delivery_status", QJsonValue("awaiting peer"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST:
+        interactionObject.insert("delivery_status", QJsonValue("awaiting host"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_TIMEOUT_EXPIRED:
+        interactionObject.insert("delivery_status", QJsonValue("awaiting peer timeout"));
+        break;
+    case lrc::api::interaction::Status::TRANSFER_FINISHED:
+        interactionObject.insert("delivery_status", QJsonValue("finished"));
+        break;
+    case lrc::api::interaction::Status::INVALID:
+    case lrc::api::interaction::Status::UNKNOWN:
+    case lrc::api::interaction::Status::UNREAD:
+    default:
+        interactionObject.insert("delivery_status", QJsonValue("unknown"));
+        break;
+    }
+    return interactionObject;
+}
+
+QString
+interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
+    const uint64_t msgId,
+    const lrc::api::interaction::Info& interaction)
+{
+    auto interactionObject = buildInteractionJson(conversationModel, msgId, interaction);
+    return QString(QJsonDocument(interactionObject).toJson(QJsonDocument::Compact));
+}
+
+QString
+interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
+    const std::map<uint64_t,
+    lrc::api::interaction::Info> interactions)
+{
+    QJsonArray array;
+    for (const auto& interaction : interactions) {
+        array.append(buildInteractionJson(conversationModel, interaction.first, interaction.second));
+    }
+    return QString(QJsonDocument(array).toJson(QJsonDocument::Compact));
+}
\ No newline at end of file
diff --git a/webchathelpers.h b/webchathelpers.h
new file mode 100644
index 0000000..6267a2c
--- /dev/null
+++ b/webchathelpers.h
@@ -0,0 +1,40 @@
+/***************************************************************************
+ * Copyright (C) 2017-2018 by Savoir-faire Linux                           *
+ * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>            *
+ * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com>            *
+ * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>              *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>          *
+
+ *                                                                         *
+ * This program is free software; you can redistribute it and/or modify    *
+ * it under the terms of the GNU General Public License as published by    *
+ * the Free Software Foundation; either version 3 of the License, or       *
+ * (at your option) any later version.                                     *
+ *                                                                         *
+ * This program is distributed in the hope that it will be useful,         *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
+ * GNU General Public License for more details.                            *
+ *                                                                         *
+ * You should have received a copy of the GNU General Public License       *
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ **************************************************************************/
+
+#pragma once
+
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QFile>
+
+#include "api/conversationmodel.h"
+
+QJsonObject buildInteractionJson(lrc::api::ConversationModel& conversationModel,
+                                 const uint64_t msgId,
+                                 const lrc::api::interaction::Info& interaction);
+QString interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
+                                           const uint64_t msgId,
+                                           const lrc::api::interaction::Info& interaction);
+QString interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
+                                      const std::map<uint64_t,
+                                      lrc::api::interaction::Info> interactions);
\ No newline at end of file
-- 
GitLab