=== added file '.bzrignore'
--- .bzrignore	1970-01-01 00:00:00 +0000
+++ .bzrignore	2016-02-05 00:58:50 +0000
@@ -0,0 +1,37 @@
+*.qmlproject.user
+CMakeCache.txt
+CMakeFiles/
+CMakeLists.txt.user
+cmake_install.cmake
+cmake_uninstall.cmake
+Makefile
+install_manifest.txt
+CTestTestfile.cmake
+
+*.cbp
+*.moc
+moc_*.cpp
+*_automoc.cpp
+
+Testing
+RE:tests/qml/tst_\w+Tests$
+po/*.gmo
+po/src
+
+config.h
+src/messaging-app
+src/messaging-app.desktop*
+
+obj-*
+debian/usr.bin.webbrowser-app
+debian/files
+debian/tmp/
+debian/qtdeclarative5-ubuntu-ui-extras-browser-plugin/
+debian/qtdeclarative5-ubuntu-web-plugin/
+debian/qtdeclarative5-ubuntu-web-plugin-doc/
+debian/messaging-app/
+debian/messaging-app-autopilot/
+debian/*.debhelper
+debian/*.debhelper.log
+debian/*.substvars
+debian/stamp-*

=== modified file 'CMakeLists.txt'
--- CMakeLists.txt	2015-10-13 15:13:32 +0000
+++ CMakeLists.txt	2016-02-05 00:58:50 +0000
@@ -35,12 +35,7 @@
 # Instruct CMake to run moc automatically when needed.
 set(CMAKE_AUTOMOC ON)
 
-configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY)
-
-#find_package(Qt5Contacts)
 find_package(Qt5DBus)
-#find_package(Qt5Gui)
-#find_package(Qt5Multimedia)
 find_package(Qt5Qml)
 find_package(Qt5Quick)
 find_package(Qt5Test)
@@ -49,23 +44,24 @@
 include(qt5)
 
 find_package(PkgConfig REQUIRED)
-#pkg_check_modules(TP_QT5 REQUIRED TelepathyQt5)
-#pkg_check_modules(TPL_QT5 REQUIRED TelepathyLoggerQt5)
-#pkg_check_modules(QTGLIB REQUIRED QtGLib-2.0)
-#pkg_check_modules(GLIB REQUIRED glib-2.0)
 pkg_check_modules(NOTIFY REQUIRED libnotify)
-#pkg_check_modules(MESSAGING_MENU REQUIRED messaging-menu)
-
-# Check if the messaging menu has the message header
-#set(CMAKE_REQUIRED_INCLUDES ${MESSAGING_MENU_INCLUDE_DIRS})
-#check_include_file("messaging-menu-message.h" HAVE_MESSAGING_MENU_MESSAGE)
-
-if (HAVE_MESSAGING_MENU_MESSAGE)
-    add_definitions(-DHAVE_MESSAGING_MENU_MESSAGE)
-endif (HAVE_MESSAGING_MENU_MESSAGE)
+
+#find unity8 qml libraries
+set(UNITY8_QML_PATH /usr/lib/${CMAKE_C_LIBRARY_ARCHITECTURE}/unity8/qml/)
+find_path(LIB_UNITY_QML_EXISTS  NAMES libUnity-qml.so
+    HINTS "${UNITY8_QML_PATH}"
+    NO_CMAKE_PATH
+    NO_CMAKE_ENVIRONMENT_PATH
+    NO_SYSTEM_ENVIRONMENT_PATH
+)
+if(!LIB_UNITY_QML_EXISTS)
+    MESSAGE(FATAL_ERROR "unity8 private package not-found")
+endif()
 
 add_definitions(-DQT_NO_KEYWORDS)
 
+configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY)
+
 include_directories(
     ${CMAKE_CURRENT_BINARY_DIR}
     ${CMAKE_CURRENT_SOURCE_DIR}

=== added file 'TODO.convergence'
--- TODO.convergence	1970-01-01 00:00:00 +0000
+++ TODO.convergence	2016-02-05 00:58:50 +0000
@@ -0,0 +1,3 @@
+- Right now if you call startChat() in the two panel layout, it won't match the
+  thread on the left side, and if you resize it to one panel, the conversation
+  is closed.

=== modified file 'config.h.in'
--- config.h.in	2014-05-28 20:28:55 +0000
+++ config.h.in	2016-02-05 00:58:50 +0000
@@ -27,6 +27,7 @@
 #include <QtDBus/QDBusReply>
 
 #define I18N_DIRECTORY "@CMAKE_INSTALL_PREFIX@/share/locale"
+#define UNITY8_QML_PATH "@UNITY8_QML_PATH@"
 
 inline bool isRunningInstalled() {
     static bool installed = (QCoreApplication::applicationDirPath() ==

=== modified file 'debian/control'
--- debian/control	2015-09-11 14:25:57 +0000
+++ debian/control	2016-02-05 00:58:50 +0000
@@ -2,8 +2,12 @@
 Section: x11
 Priority: optional
 Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
-Build-Depends: cmake,
+Build-Depends: apparmor,
+               apparmor-easyprof,
+               apparmor-easyprof-ubuntu (>= 1.3.13),
+               cmake,
                debhelper (>= 9),
+               dh-apparmor,
                dh-translations,
                libnotify-dev,
                python-flake8,
@@ -21,8 +25,12 @@
                qtdeclarative5-ubuntu-telephony0.1 | qtdeclarative5-ubuntu-telephony-plugin,
                qtdeclarative5-ubuntu-content1,
                qtdeclarative5-ubuntu-addressbook0.1,
+               qtdeclarative5-ubuntu-thumbnailer0.1,
                qtdeclarative5-qtcontacts-plugin,
+               qtdeclarative5-folderlistmodel-plugin,
+               qtmultimedia5-dev,
                qml-module-qt-labs-settings,
+               qml-module-qtmultimedia,
                qtpim5-dev,
                xvfb,
 Standards-Version: 3.9.4
@@ -37,6 +45,7 @@
 Architecture: any
 Depends: ${misc:Depends},
          ${shlibs:Depends},
+         libqt5multimedia5,
          qtdeclarative5-ubuntu-addressbook0.1,
          qtdeclarative5-ubuntu-ui-toolkit-plugin | qt-components-ubuntu,
          qtdeclarative5-ubuntu-telephony-phonenumber0.1,

=== added file 'debian/messaging-app-apparmor.additions'
--- debian/messaging-app-apparmor.additions	1970-01-01 00:00:00 +0000
+++ debian/messaging-app-apparmor.additions	2016-02-05 00:58:50 +0000
@@ -0,0 +1,86 @@
+  @{HOME}/.local/share/history-service/attachments/** r,
+
+  # Description: Can access the telephony-service and related services
+  # Usage: reserved
+
+  # grant full access to telephony service handler and indicator
+  dbus (receive, send)
+       bus=session
+       peer=(name=com.canonical.TelephonyServiceHandler,label=unconfined),
+
+  dbus (receive, send)
+       bus=session
+       peer=(name=com.canonical.TelephonyServiceIndicator,label=unconfined),
+
+  # make it possible for apps to register a telepathy observer
+  dbus bind
+       bus=session
+       name=org.freedesktop.Telepathy.Client.*,
+
+  dbus (send)
+       interface="org.freedesktop.Notifications"
+       member="GetServerInformation",
+
+  dbus (send)
+       interface="org.freedesktop.Notifications"
+       member="Notify",
+
+  # query greeter status
+  dbus (receive, send)
+       bus=session
+       peer=(name=com.canonical.UnityGreeter,label=unconfined),
+
+  # access to telepathy-ofono and other telepathy clients/managers
+  dbus (receive, send)
+       bus=session
+       path=/org/freedesktop/Telepathy/**,
+
+  dbus (send)
+       bus=session
+       path="/ca/desrt/dconf/Writer/user"
+       interface="ca.desrt.dconf.Writer",
+
+  # used to query ringtone files and other properties
+  dbus (receive, send)
+       bus=system
+       path="/org/freedesktop/Accounts/User[0-9]*"
+       interface=org.freedesktop.DBus.Properties,
+
+  dbus (receive, send)
+       bus=session
+       path="/org/freedesktop/DBus"
+       interface=org.freedesktop.DBus,
+
+  # used by libtelephony-service to order/query existing modems
+  dbus (send)
+       bus=system
+       interface="org.ofono.Manager",
+
+  # used by apps to get sim contacts
+  dbus (send)
+       bus=system
+       interface="org.ofono.SimManager",
+
+  dbus (send)
+       bus=system
+       interface="org.ofono.Modem",
+
+  dbus (send, receive)
+       bus=session
+       peer=(name=com.meego.msyncd,label=unconfined),
+
+  # used by telepathy-qt to guess existing managers and their features
+  /usr/share/telepathy/managers/* r,
+  # read protocol files and assets
+  /usr/share/telephony-service/** r,
+
+  # Description: Can access urfkill
+  # Usage: common
+  #include <abstractions/nameservice>
+
+  allow dbus (receive, send)
+       bus=system
+       path=/org/freedesktop/URfkill,
+  allow dbus (receive, send)
+       bus=system
+       peer=(name=org.freedesktop.URfkill),

=== added file 'debian/messaging-app-apparmor.manifest'
--- debian/messaging-app-apparmor.manifest	1970-01-01 00:00:00 +0000
+++ debian/messaging-app-apparmor.manifest	2016-02-05 00:58:50 +0000
@@ -0,0 +1,37 @@
+{
+  "profiles": {
+    "messaging-app": {
+      "binary": "/usr/bin/messaging-app",
+      "profile_name": "messaging-app",
+      "policy_vendor": "ubuntu",
+      "policy_version": 1.3,
+      "policy_groups": [
+        "accounts",
+        "audio",
+        "contacts",
+        "content_exchange",
+        "content_exchange_source",
+        "history",
+        "microphone",
+        "video"
+      ],
+      "abstractions": [
+        "user-tmp"
+      ],
+      "template_variables": {
+        "APP_ID_DBUS": "messaging_2dapp",
+        "APP_PKGNAME_DBUS": "messaging_2dapp",
+        "APP_PKGNAME": "com.ubuntu.messaging-app"
+      },
+      "read_path": [
+        "/usr/share/applications/",
+        "/custom/xdg/data/dconf/",
+        "/usr/share/*/assets/",
+        "@{HOME}/.local/share/evolution/addressbook/*/photos/",
+        "@{HOME}/.cache/messaging-app/HubIncoming/**",
+        "@{HOME}/.config/dconf/user",
+        "/usr/share/messaging-app/"
+      ]
+    }
+  }
+}

=== modified file 'debian/messaging-app.install'
--- debian/messaging-app.install	2014-08-11 22:22:59 +0000
+++ debian/messaging-app.install	2016-02-05 00:58:50 +0000
@@ -8,4 +8,6 @@
 usr/share/messaging-app/3rd_party
 usr/share/messaging-app/MMS/*.qml
 usr/share/messaging-app/Dialogs/*.qml
+usr/share/messaging-app/Stickers/*.qml
 usr/bin/*messaging-app*
+debian/usr.bin.messaging-app etc/apparmor.d

=== modified file 'debian/rules'
--- debian/rules	2014-08-26 21:26:33 +0000
+++ debian/rules	2016-02-05 00:58:50 +0000
@@ -4,11 +4,33 @@
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
 
+export DEB_BUILD_HARDENING=1
 export DPKG_GENSYMBOLS_CHECK_LEVEL=4
 
 %:
 	dh $@ --parallel --fail-missing --with translations
 
+apparmor:
+	aa-easyprof -m ./debian/messaging-app-apparmor.manifest --no-verify | \
+	egrep -v '(# Click packages|CLICK_DIR)' | \
+	sed 's/@{APP_PKGNAME}_@{APP_APPNAME}_@{APP_VERSION}/@{APP_PKGNAME}/g' | \
+	sed 's,Apps/@{APP_PKGNAME},Apps/messaging-app,g' | \
+	sed '/lttng-ust-/c\  \/{,var\/}run\/shm\/lttng-ust-* r,'  | \
+	sed '/dconf.user rw/c\  \/run\/user\/\[0-9\]*\/dconf\/user rw,' \
+	> ./debian/usr.bin.messaging-app
+	(head -n -2 ./debian/usr.bin.messaging-app; cat ./debian/messaging-app-apparmor.additions; \
+	echo } ) > ./debian/usr.bin.messaging-app2
+	mv ./debian/usr.bin.messaging-app2 ./debian/usr.bin.messaging-app
+	apparmor_parser -QTK ./debian/usr.bin.messaging-app
+
+override_dh_install: apparmor
+	dh_install --fail-missing
+
+override_dh_installdeb:
+	dh_apparmor --profile-name=usr.bin.messaging-app -pmessaging-app
+	dh_installdeb
+
+
 override_dh_auto_test:
 	flake8  tests/autopilot/messaging_app/
 	cd obj-$(DEB_HOST_GNU_TYPE); ctest -V

=== modified file 'src/CMakeLists.txt'
--- src/CMakeLists.txt	2015-02-24 13:33:31 +0000
+++ src/CMakeLists.txt	2016-02-05 00:58:50 +0000
@@ -1,18 +1,24 @@
 set(MESSAGING_APP messaging-app)
 
 set(messaging_app_HDRS
+    audiorecorder.h
+    fileoperations.h
     messagingapplication.h
+    stickers-history-model.h
     )
 
 set(messaging_app_SRCS
+    audiorecorder.cpp
+    fileoperations.cpp
     messagingapplication.cpp
     main.cpp
+    stickers-history-model.cpp
     )
 
 add_executable(${MESSAGING_APP}
     ${messaging_app_SRCS}
     )
-qt5_use_modules(${MESSAGING_APP} Core DBus Gui Qml Quick Versit)
+qt5_use_modules(${MESSAGING_APP} Core DBus Gui Multimedia Qml Quick Sql Versit)
 
 include_directories(
     ${CMAKE_CURRENT_BINARY_DIR}

=== added file 'src/audiorecorder.cpp'
--- src/audiorecorder.cpp	1970-01-01 00:00:00 +0000
+++ src/audiorecorder.cpp	2016-02-05 00:58:50 +0000
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * Authors:
+ *  Arthur Renato Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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 "audiorecorder.h"
+
+#include <QDebug>
+#include <QDir>
+#include <QUrl>
+#include <QStandardPaths>
+#include <QTemporaryFile>
+
+AudioRecorder::AudioRecorder(QObject *parent)
+    : QObject(parent)
+{
+    m_audioRecorder = new QAudioRecorder();
+    connect(m_audioRecorder, SIGNAL(stateChanged(QMediaRecorder::State)),
+            SIGNAL(recorderStateChanged()));
+    connect(m_audioRecorder, SIGNAL(statusChanged(QMediaRecorder::Status)),
+            SIGNAL(recorderStatusChanged()));
+    connect(m_audioRecorder, SIGNAL(error(QMediaRecorder::Error)),
+            SLOT(updateRecorderError(QMediaRecorder::Error)));
+    connect(m_audioRecorder, SIGNAL(actualLocationChanged(QUrl)),
+            SLOT(updateActualLocation(QUrl)));
+    connect(m_audioRecorder, SIGNAL(durationChanged(qint64)), SIGNAL(durationChanged(qint64)));
+    connect(m_audioRecorder, SIGNAL(audioInputChanged(const QString&)),
+            SIGNAL(audioInputChanged(const QString&)));
+
+    m_audioSettings = m_audioRecorder->audioSettings();
+}
+
+AudioRecorder::~AudioRecorder()
+{
+    delete m_audioRecorder;
+}
+
+AudioRecorder::RecorderState AudioRecorder::recorderState() const
+{
+    return RecorderState(m_audioRecorder->state());
+}
+
+AudioRecorder::RecorderStatus AudioRecorder::recorderStatus() const
+{
+    return RecorderStatus(m_audioRecorder->status());
+}
+
+AudioRecorder::Error AudioRecorder::errorCode() const
+{
+    return Error(m_audioRecorder->error());
+}
+
+QString AudioRecorder::errorString() const
+{
+    return m_audioRecorder->errorString();
+}
+
+QString AudioRecorder::outputLocation() const
+{
+    return m_audioRecorder->outputLocation().toString();
+}
+
+QString AudioRecorder::actualLocation() const
+{
+    return m_audioRecorder->actualLocation().toString();
+}
+
+qint64 AudioRecorder::duration() const
+{
+    return m_audioRecorder->duration();
+}
+
+int AudioRecorder::bitRate() const
+{
+    return m_audioSettings.bitRate();
+}
+
+int AudioRecorder::channelCount() const
+{
+    return m_audioSettings.channelCount();
+}
+
+QString AudioRecorder::codec() const
+{
+    return m_audioSettings.codec();
+}
+
+AudioRecorder::EncodingQuality AudioRecorder::quality() const
+{
+    return EncodingQuality(m_audioSettings.quality());
+}
+
+int AudioRecorder::sampleRate() const
+{
+    return m_audioSettings.sampleRate();
+}
+
+QString AudioRecorder::audioInput() const
+{
+    return m_audioRecorder->audioInput();
+}
+
+void AudioRecorder::record()
+{
+    setRecorderState(RecordingState);
+}
+
+void AudioRecorder::stop()
+{
+    setRecorderState(StoppedState);
+}
+
+void AudioRecorder::pause()
+{
+    setRecorderState(PausedState);
+}
+
+void AudioRecorder::setRecorderState(AudioRecorder::RecorderState state)
+{
+    if (!m_audioRecorder)
+        return;
+
+    switch (state){
+        case AudioRecorder::RecordingState: {
+            // Create temporary file to store audio recorded
+            QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+            QTemporaryFile outputFile(dataLocation.absoluteFilePath("audioXXXXXX%1").arg(m_fileExtension));
+            outputFile.setAutoRemove(false);
+            outputFile.open();
+            outputFile.close();
+            setOutputLocation(outputFile.fileName());
+
+            m_audioRecorder->record();
+            break;
+        }
+        case AudioRecorder::StoppedState:
+            m_audioRecorder->stop();
+            break;
+        case AudioRecorder::PausedState:
+            m_audioRecorder->pause();
+            break;
+    }
+}
+
+void AudioRecorder::setOutputLocation(const QString &location)
+{
+    if (outputLocation() != location) {
+        // FIXME: implement auto-removal of previous recordings
+        m_audioRecorder->setOutputLocation(location);
+        Q_EMIT outputLocationChanged(outputLocation());
+    }
+}
+
+void AudioRecorder::setBitRate(int rate)
+{
+    if (bitRate() != rate) {
+        m_audioSettings.setBitRate(rate);
+        Q_EMIT bitRateChanged(rate);
+    }
+}
+
+void AudioRecorder::setChannelCount(int count)
+{
+    if (channelCount() != count) {
+        m_audioSettings.setChannelCount(count);
+        Q_EMIT channelCountChanged(count);
+    }
+}
+
+void AudioRecorder::setCodec(const QString &audioCodec)
+{
+    if (codec() != audioCodec) {
+        if (!m_audioRecorder->supportedAudioCodecs().contains(audioCodec)) {
+            qWarning() << "AudioRecorder error: Unsupported Audio Codec: " << audioCodec;
+            return;
+        }
+
+        if (audioCodec == "audio/vorbis" ||
+            audioCodec == "audio/speex" ||
+            audioCodec == "audio/FLAC") {
+
+            m_audioRecorder->setContainerFormat("ogg");
+            m_fileExtension = ".ogg";
+        } else if (audioCodec == "audio/PCM") {
+            m_audioRecorder->setContainerFormat("wav");
+            m_fileExtension = ".wav";
+        } else {
+            m_audioRecorder->setContainerFormat("raw");
+        }
+
+        m_audioSettings.setCodec(audioCodec);
+        Q_EMIT codecChanged(audioCodec);
+    }
+}
+
+void AudioRecorder::setQuality(AudioRecorder::EncodingQuality encodingQuality)
+{
+    if (quality() != encodingQuality) {
+        m_audioSettings.setQuality(QMultimedia::EncodingQuality(encodingQuality));
+        Q_EMIT qualityChanged(encodingQuality);
+    }
+}
+
+void AudioRecorder::setSampleRate(int rate)
+{
+    if (sampleRate() != rate) {
+        m_audioSettings.setSampleRate(rate);
+        Q_EMIT sampleRateChanged(rate);
+    }
+}
+
+void AudioRecorder::setAudioInput(const QString &input)
+{
+    if (audioInput() != input) {
+        m_audioRecorder->setAudioInput(input);
+        Q_EMIT audioInputChanged(input);
+    }
+}
+
+void AudioRecorder::updateRecorderError(QMediaRecorder::Error errorCode)
+{
+    qWarning() << "AudioRecorder error:" << errorString();
+    Q_EMIT errorChanged(Error(errorCode), errorString());
+}
+
+void AudioRecorder::updateActualLocation(const QUrl &url)
+{
+    Q_EMIT actualLocationChanged(url.toString());
+}

=== added file 'src/audiorecorder.h'
--- src/audiorecorder.h	1970-01-01 00:00:00 +0000
+++ src/audiorecorder.h	2016-02-05 00:58:50 +0000
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * Authors:
+ *  Arthur Renato Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+#ifndef AUDIORECORDER_H
+#define AUDIORECORDER_H
+
+#include <QObject>
+#include <QAudioRecorder>
+
+class AudioRecorder : public QObject
+{
+    Q_OBJECT
+
+    Q_ENUMS(EncodingQuality)
+    Q_ENUMS(Error)
+    Q_ENUMS(RecorderState)
+    Q_ENUMS(RecorderStatus)
+
+    Q_PROPERTY(RecorderState recorderState READ recorderState WRITE setRecorderState NOTIFY recorderStateChanged)
+    Q_PROPERTY(RecorderStatus recorderStatus READ recorderStatus NOTIFY recorderStatusChanged)
+    Q_PROPERTY(QString errorString READ errorString NOTIFY errorChanged)
+    Q_PROPERTY(Error errorCode READ errorCode NOTIFY errorChanged)
+    Q_PROPERTY(QString outputLocation READ outputLocation NOTIFY outputLocationChanged)
+    Q_PROPERTY(QString actualLocation READ actualLocation NOTIFY actualLocationChanged)
+    Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
+    Q_PROPERTY(int bitRate READ bitRate WRITE setBitRate NOTIFY bitRateChanged);
+    Q_PROPERTY(int channelCount READ channelCount WRITE setChannelCount NOTIFY channelCountChanged);
+    Q_PROPERTY(QString codec READ codec WRITE setCodec NOTIFY codecChanged);
+    Q_PROPERTY(EncodingQuality quality READ quality WRITE setQuality NOTIFY qualityChanged);
+    Q_PROPERTY(int sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged);
+    Q_PROPERTY(QString audioInput READ audioInput WRITE setAudioInput NOTIFY audioInputChanged);
+
+public:
+    enum EncodingQuality
+    {
+        VeryLowQuality = QMultimedia::VeryLowQuality,
+        LowQuality = QMultimedia::LowQuality,
+        NormalQuality = QMultimedia::NormalQuality,
+        HighQuality = QMultimedia::HighQuality,
+        VeryHighQuality = QMultimedia::VeryHighQuality
+    };
+
+    enum Error
+    {
+        NoError = QMediaRecorder::NoError,
+        ResourceError = QMediaRecorder::ResourceError,
+        FormatError = QMediaRecorder::FormatError,
+        OutOfSpaceError = QMediaRecorder::OutOfSpaceError
+    };
+ 
+    enum RecorderState
+    {
+        StoppedState = QMediaRecorder::StoppedState,
+        RecordingState = QMediaRecorder::RecordingState,
+        PausedState = QMediaRecorder::PausedState
+    };
+
+    enum RecorderStatus
+    {
+        UnavailableStatus = QMediaRecorder::UnavailableStatus,
+        UnloadedStatus = QMediaRecorder::UnloadedStatus,
+        LoadingStatus = QMediaRecorder::LoadingStatus,
+        LoadedStatus = QMediaRecorder::LoadedStatus,
+        StartingStatus = QMediaRecorder::StartingStatus,
+        RecordingStatus = QMediaRecorder::RecordingStatus,
+        PausedStatus = QMediaRecorder::PausedStatus,
+        FinalizingStatus = QMediaRecorder::FinalizingStatus
+    };
+
+    AudioRecorder(QObject *parent = 0);
+    ~AudioRecorder();
+
+    RecorderState recorderState() const;
+    RecorderStatus recorderStatus() const;
+    Error errorCode() const;
+    QString errorString() const;
+    QString outputLocation() const;
+    QString actualLocation() const;
+    qint64 duration() const;
+    int bitRate() const;
+    int channelCount() const;
+    QString codec() const;
+    EncodingQuality quality() const;
+    int sampleRate() const;
+    QString audioInput() const;
+
+public Q_SLOTS:
+    void record();
+    void stop();
+    void pause();
+    void setRecorderState(AudioRecorder::RecorderState state);
+    void setOutputLocation(const QString &location);
+    void setBitRate(int rate);
+    void setChannelCount(int count);
+    void setCodec(const QString &audioCodec);
+    void setQuality(AudioRecorder::EncodingQuality encodingQuality);
+    void setSampleRate(int rate);
+    void setAudioInput(const QString &input);
+
+Q_SIGNALS:
+    void recorderStateChanged();
+    void recorderStatusChanged();
+    void errorChanged(AudioRecorder::Error errorCode, const QString &errorString);
+    void outputLocationChanged(const QString &location);
+    void actualLocationChanged(const QString &location);
+    void durationChanged(qint64 duration);
+    void bitRateChanged(int rate);
+    void channelCountChanged(int count);
+    void codecChanged(const QString &codec);
+    void qualityChanged(AudioRecorder::EncodingQuality quality);
+    void sampleRateChanged(int rate);
+    void audioInputChanged(const QString &input);
+
+private Q_SLOTS:
+    void updateRecorderError(QMediaRecorder::Error);
+    void updateActualLocation(const QUrl&);
+
+private:
+    QAudioRecorder *m_audioRecorder;
+    QAudioEncoderSettings m_audioSettings;
+    QString m_fileExtension;
+};
+
+#endif // AUDIORECORDER_H

=== added file 'src/fileoperations.cpp'
--- src/fileoperations.cpp	1970-01-01 00:00:00 +0000
+++ src/fileoperations.cpp	2016-02-05 00:58:50 +0000
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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 "fileoperations.h"
+
+#include <QDir>
+#include <QFile>
+#include <QTemporaryFile>
+#include <QStandardPaths>
+
+FileOperations::FileOperations(QObject *parent)
+    : QObject(parent)
+{
+}
+
+FileOperations::~FileOperations()
+{
+}
+
+QString FileOperations::getTemporaryFile(const QString &fileExtension) const
+{
+    //TODO remove once lp:1420728 is fixed
+    QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
+    if (!dataLocation.exists()) {
+        dataLocation.mkpath(".");
+    }
+    QTemporaryFile tmp(dataLocation.path() + "/tmpXXXXXX" + fileExtension);
+    tmp.open();
+    return tmp.fileName();
+}
+
+bool FileOperations::link(const QString &from, const QString &to)
+{
+    return QFile::link(from, to);
+}
+
+bool FileOperations::remove(const QString &fileName)
+{
+    return QFile::remove(fileName);
+}
+
+qint64 FileOperations::size(const QString &filePath)
+{
+    return QFile(filePath).size();
+}

=== added file 'src/fileoperations.h'
--- src/fileoperations.h	1970-01-01 00:00:00 +0000
+++ src/fileoperations.h	2016-02-05 00:58:50 +0000
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+#ifndef FILEOPERATIONS_H
+#define FILEOPERATIONS_H
+
+#include <QObject>
+
+class FileOperations : public QObject
+{
+    Q_OBJECT
+
+public:
+    FileOperations(QObject *parent = 0);
+    ~FileOperations();
+
+    Q_INVOKABLE QString getTemporaryFile(const QString &fileExtension) const;
+    Q_INVOKABLE bool link(const QString &from, const QString &to);
+    Q_INVOKABLE bool remove(const QString &fileName);
+    Q_INVOKABLE qint64 size(const QString &filePath);
+};
+
+#endif // FILEOPERATIONS_H

=== modified file 'src/messagingapplication.cpp'
--- src/messagingapplication.cpp	2015-11-23 19:51:16 +0000
+++ src/messagingapplication.cpp	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 Canonical, Ltd.
+ * Copyright (C) 2012-2015 Canonical, Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -17,6 +17,9 @@
  */
 
 #include "messagingapplication.h"
+#include "audiorecorder.h"
+#include "fileoperations.h"
+#include "stickers-history-model.h"
 
 #include <libnotify/notify.h>
 
@@ -24,6 +27,7 @@
 #include <QUrl>
 #include <QUrlQuery>
 #include <QDebug>
+#include <QDir>
 #include <QStringList>
 #include <QQuickItem>
 #include <QQmlComponent>
@@ -36,6 +40,7 @@
 #include "config.h"
 #include <QQmlEngine>
 #include <QMimeDatabase>
+#include <QStandardPaths>
 #include <QVersitReader>
 
 using namespace QtVersit;
@@ -65,12 +70,31 @@
     }
 }
 
+static QObject* FileOperations_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
+{
+    Q_UNUSED(engine);
+    Q_UNUSED(scriptEngine);
+    return new FileOperations();
+}
+
+static QObject* StickersHistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
+{
+    Q_UNUSED(engine);
+    Q_UNUSED(scriptEngine);
+    return new StickersHistoryModel();
+}
+
 MessagingApplication::MessagingApplication(int &argc, char **argv)
     : QGuiApplication(argc, argv), m_view(0), m_applicationIsReady(false)
 {
     setApplicationName("MessagingApp");
 }
 
+bool MessagingApplication::fullscreen() const
+{
+    return m_view->windowState() == Qt::WindowFullScreen;
+}
+
 bool MessagingApplication::setup()
 {
     installIconPath();
@@ -141,6 +165,7 @@
     m_view->rootContext()->setContextProperty("application", this);
     m_view->rootContext()->setContextProperty("i18nDirectory", I18N_DIRECTORY);
     m_view->engine()->setBaseUrl(QUrl::fromLocalFile(messagingAppDirectory()));
+    m_view->engine()->addImportPath(UNITY8_QML_PATH);
 
     // check if there is a contacts backend override
     QString contactsBackend = qgetenv("QTCONTACTS_MANAGER_OVERRIDE");
@@ -149,6 +174,14 @@
         m_view->rootContext()->setContextProperty("QTCONTACTS_MANAGER_OVERRIDE", contactsBackend);
     }
 
+    QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+    m_view->rootContext()->setContextProperty("dataLocation", dataLocation.absolutePath());
+    dataLocation.mkpath("stickers");
+    const char* uri = "messagingapp.private";
+    qmlRegisterType<AudioRecorder>(uri, 0, 1, "AudioRecorder");
+    qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
+    qmlRegisterSingletonType<StickersHistoryModel>(uri, 0, 1, "StickersHistoryModel", StickersHistoryModel_singleton_factory);
+
     // used by autopilot tests to load vcards during tests
     QByteArray testData = qgetenv("QTCONTACTS_PRELOAD_VCARD");
     m_view->rootContext()->setContextProperty("QTCONTACTS_PRELOAD_VCARD", testData);
@@ -176,6 +209,17 @@
     }
 }
 
+void MessagingApplication::setFullscreen(bool fullscreen)
+{
+    if (fullscreen) {
+        m_view->setWindowState(Qt::WindowFullScreen);
+    } else {
+        m_view->setWindowState(Qt::WindowNoState);
+    }
+
+    Q_EMIT fullscreenChanged();
+}
+
 void MessagingApplication::onViewStatusChanged(QQuickView::Status status)
 {
     if (status != QQuickView::Ready) {

=== modified file 'src/messagingapplication.h'
--- src/messagingapplication.h	2015-11-18 16:36:51 +0000
+++ src/messagingapplication.h	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2013 Canonical, Ltd.
+ * Copyright (C) 2012-2015 Canonical, Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -26,13 +26,18 @@
 class MessagingApplication : public QGuiApplication
 {
     Q_OBJECT
+    Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged)
 
 public:
     MessagingApplication(int &argc, char **argv);
     virtual ~MessagingApplication();
 
+    bool fullscreen() const;
     bool setup();
 
+Q_SIGNALS:
+    void fullscreenChanged();
+
 public Q_SLOTS:
     void activateWindow();
     void parseArgument(const QString &arg);
@@ -41,6 +46,7 @@
     void showNotificationMessage(const QString &message, const QString &icon = QString());
 
 private Q_SLOTS:
+    void setFullscreen(bool fullscreen);
     void onViewStatusChanged(QQuickView::Status status);
     void onApplicationReady();
 

=== modified file 'src/qml/AccountSectionDelegate.qml'
--- src/qml/AccountSectionDelegate.qml	2015-09-14 13:51:27 +0000
+++ src/qml/AccountSectionDelegate.qml	2016-02-05 00:58:50 +0000
@@ -28,7 +28,8 @@
     property var messageData: null
     property int index: -1
     property Item delegateItem
-    property string accountLabel: telepathyHelper.accountForId(messageData.accountId).displayName
+    property var account: telepathyHelper.accountForId(messageData.accountId)
+    property string accountLabel: account ? account.displayName : ""
 
     // update the accountLabel when the list of accounts become available
     Item {

=== added file 'src/qml/AttachmentPanel.qml'
--- src/qml/AttachmentPanel.qml	1970-01-01 00:00:00 +0000
+++ src/qml/AttachmentPanel.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3 as ListItem
+import QtQuick.Layouts 1.0
+
+Item {
+    id: panel
+    signal attachmentAvailable(var attachment)
+
+    property bool expanded: false
+
+    function show() {
+        expanded = true
+    }
+
+    function hide() {
+        expanded = false
+    }
+
+    height: expanded ? childrenRect.height + units.gu(3): 0
+    opacity: expanded ? 1 : 0
+    visible: opacity > 0
+    Behavior on height {
+        UbuntuNumberAnimation {}
+    }
+    Behavior on opacity {
+        UbuntuNumberAnimation { }
+    }
+
+    enabled: expanded
+
+    Connections {
+        target: Qt.inputMethod
+        onVisibleChanged: {
+            if (Qt.inputMethod.visible) {
+                panel.expanded = false
+            }
+        }
+    }
+
+    ContentImport {
+        id: contentImporter
+
+        onContentReceived: {
+            var attachment = {}
+            var filePath = String(contentUrl).replace('file://', '')
+            attachment["contentType"] = application.fileMimeType(filePath)
+            attachment["name"] = filePath.split('/').reverse()[0]
+            attachment["filePath"] = filePath
+            panel.attachmentAvailable(attachment)
+            hide()
+        }
+    }
+
+    ListItem.ThinDivider {
+        id: divider
+        anchors {
+            top: parent.top
+            left: parent.left
+            right: parent.right
+        }
+    }
+
+    GridLayout {
+        id: grid
+
+        property int iconSize: units.gu(3)
+        property int buttonSpacing: units.gu(2)
+        anchors {
+            top: parent.top
+            topMargin: units.gu(3)
+            left: parent.left
+            right: parent.right
+        }
+
+        height: childrenRect.height
+        columns: 4
+        rowSpacing: units.gu(3)
+
+        TransparentButton {
+            id: pictureButton
+            objectName: "pictureButton"
+            iconName: "stock_image"
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("Image")
+            Layout.alignment: Qt.AlignHCenter
+            onClicked: {
+                contentImporter.requestPicture()
+            }
+        }
+
+        TransparentButton {
+            id: videoButton
+            objectName: "videoButton"
+            iconName: "stock_video"
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("Video")
+            Layout.alignment: Qt.AlignHCenter
+            onClicked: {
+                contentImporter.requestVideo()
+            }
+        }
+
+        // FIXME: enable generic file sharing if we ever support it
+        /*TransparentButton {
+            id: fileButton
+            objectName: "fileButton"
+            iconSource: Qt.resolvedUrl("assets/stock_document.svg")
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("File")
+            Layout.alignment: Qt.AlignHCenter
+            onClicked: {
+                contentImporter.requestDocument()
+            }
+        }*/
+
+        // FIXME: enable location sharing if we ever support it
+        /*TransparentButton {
+            id: locationButton
+            objectName: "locationButton"
+            iconName: "location"
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("Location")
+            Layout.alignment: Qt.AlignHCenter
+        }*/
+
+        TransparentButton {
+            id: contactButton
+            objectName: "contactButton"
+            iconName: "stock_contact"
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("Contact")
+            Layout.alignment: Qt.AlignHCenter
+            onClicked: {
+                contentImporter.requestContact()
+            }
+        }
+
+        // FIXME: enable that once we add support for burn-after-read
+        /*TransparentButton {
+            id: burnAfterReadButton
+            objectName: "burnAfterReadButton"
+            iconSource: Qt.resolvedUrl("assets/burn-after-read.svg")
+            iconSize: grid.iconSize
+            spacing: grid.buttonSpacing
+            text: i18n.tr("Burn after read")
+            Layout.alignment: Qt.AlignHCenter
+        }*/
+    }
+}
+

=== added file 'src/qml/AudioPlaybackBar.qml'
--- src/qml/AudioPlaybackBar.qml	1970-01-01 00:00:00 +0000
+++ src/qml/AudioPlaybackBar.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import QtMultimedia 5.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Themes.Ambiance 1.3
+import "dateUtils.js" as DateUtils
+
+Item {
+    id: playbackBar
+
+    signal resetRequested()
+    property string source: ""
+    property int duration: audioPlayer.duration
+    readonly property bool playing: audioPlayer.playing
+
+    Loader {
+        id: audioPlayer
+        readonly property bool playing: ready ? item.playing : false
+        readonly property bool paused: ready ? item.paused : false
+        readonly property bool stopped: ready ? item.stopped : false
+        readonly property int position: ready ? item.position : 0
+        readonly property int duration: ready ? item.duration : 0
+        readonly property bool ready: status == Loader.Ready
+        readonly property int playbackState: ready ? item.playbackState : Audio.StoppedState
+        function play() { 
+            audioPlayer.active = true
+            item.play() 
+        }
+        function stop() {
+            item.stop()
+            audioPlayer.active = false
+        }
+        function pause() { item.pause() }
+        function seek(pos) { item.seek(pos) }
+        active: false
+        sourceComponent: audioPlayerComponent
+    }
+
+    Component {
+        id: audioPlayerComponent
+        Audio {
+            id: audioPlayer1
+            readonly property bool playing: audioPlayer1.playbackState == Audio.PlayingState
+            readonly property bool paused: audioPlayer1.playbackState == Audio.PausedState
+            readonly property bool stopped: audioPlayer1.playbackState == Audio.StoppedState
+            source: playbackBar.source
+        }
+    }
+
+    TransparentButton {
+        id: closeButton
+        objectName: "closeButton"
+
+        anchors {
+            left: parent.left
+            leftMargin: units.gu(2)
+            verticalCenter: parent.verticalCenter
+        }
+
+        iconName: "close"
+
+        onClicked: {
+            playbackBar.resetRequested()
+        }
+    }
+
+    Item {
+        id: audioPreview
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+            left: closeButton.right
+            right: parent.right
+            topMargin: units.gu(1)
+            bottomMargin: units.gu(1)
+            leftMargin: units.gu(3)
+            rightMargin: units.gu(1)
+        }
+
+        TransparentButton {
+            id: playButton
+
+            anchors {
+                top: parent.top
+                left: parent.left
+                topMargin: units.gu(0.5)
+            }
+
+            iconColor: "grey"
+            iconName: audioPlayer.playing ? "media-playback-pause" : "media-playback-start"
+
+            textSize: FontUtils.sizeToPixels("x-small")
+            text: {
+                if (audioPlayer.playing) {
+                    return DateUtils.formattedTime(audioPlayer.position/ 1000)
+                }
+                return DateUtils.formattedTime(playbackBar.duration / 1000)
+            }
+
+            onClicked: {
+                if (audioPlayer.playing) {
+                    audioPlayer.pause()
+                } else {
+                    audioPlayer.play()
+                }
+            }
+        }
+
+        Slider {
+            id: slider
+            Connections {
+                target: audioPlayer
+                onDurationChanged: {
+                    if (slider.maximumValue == 100) {
+                        slider.maximumValue = audioPlayer.duration
+                    }
+                }
+            }
+            style: SliderStyle {
+                Component.onCompleted: thumb.visible = false
+                Connections {
+                    target: audioPlayer
+                    onPlaybackStateChanged: {
+                        thumb.visible = !audioPlayer.stopped
+                        if (!thumb.visible) {
+                            audioPlayer.seek(0)
+                        }
+                    }
+                }
+            }
+            enabled: !audioPlayer.stopped
+            function formatValue(v) { return DateUtils.formattedTime(v/1000) }
+            anchors {
+                top: parent.top
+                bottom: parent.bottom
+                left: playButton.right
+                right: parent.right
+                leftMargin: units.gu(1)
+            }
+            height: units.gu(3)
+            minimumValue: 0.0
+            maximumValue: 100
+            value: audioPlayer.position
+            activeFocusOnPress: false
+            onPressedChanged: {
+                if (!pressed) {
+                    if (audioPlayer.playing || audioPlayer.paused) {
+                        audioPlayer.seek(value)
+                    } else {
+                        audioPlayer.muted = true
+                        // we only get the duration while playing
+                        audioPlayer.play()
+                        audioPlayer.pause()
+                        if (audioPlayer.duration == 100) {
+                            audioPlayer.seek((audioPlayer.duration*value)/100)
+                        } else {
+                            audioPlayer.seek(value)
+                        }
+                        audioPlayer.muted = false
+
+                    }
+                    value = Qt.binding(function(){ return audioPlayer.position})
+                }
+            }
+        }
+    }
+
+
+}
+

=== added file 'src/qml/AudioRecordingBar.qml'
--- src/qml/AudioRecordingBar.qml	1970-01-01 00:00:00 +0000
+++ src/qml/AudioRecordingBar.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import messagingapp.private 0.1
+import "dateUtils.js" as DateUtils
+
+Item {
+    id: recordingBar
+    opacity: audioRecorder.recording ? 1.0 : 0.0
+    Behavior on opacity { UbuntuNumberAnimation {} }
+    visible: opacity > 0
+    property bool handleError: false
+
+    property int duration: 0
+    readonly property bool recording: audioRecorder.recording
+    property real buttonOpacity: 1
+
+    signal audioRecorded(var audio)
+
+    function startRecording() {
+        handleError = true
+        audioRecorder.record()
+    }
+
+    function stopRecording() {
+        audioRecorder.stop()
+    }
+
+    Loader {
+        id: audioRecorder
+        readonly property bool ready: status == Loader.Ready
+        readonly property bool recording: ready ? item.recorderStatus == AudioRecorder.RecordingStatus : false
+        readonly property int duration: ready ? item.duration : 0
+        function record() {
+            audioRecorder.active = true
+            item.record()
+        }
+        function stop() {
+            item.stop()
+            audioRecorder.active = false
+        }
+ 
+        active: false
+        sourceComponent: audioRecorderComponent
+    }
+
+    Component {
+        id: audioRecorderComponent
+        AudioRecorder {
+            readonly property bool recording: recorderStatus == AudioRecorder.RecordingStatus
+
+            onRecorderStatusChanged: {
+                if (recorderState == AudioRecorder.StoppedState && actualLocation != "") {
+                    var filePath = actualLocation
+
+                    if (application.fileMimeType(filePath).toLowerCase().indexOf("audio/") <= -1) {
+                        //If the recording process is too quick the generated file is not an audio one and should be ignored
+                        return;
+                    }
+
+                    var attachment = {}
+                    attachment["contentType"] = application.fileMimeType(filePath)
+                    attachment["name"] = filePath.split('/').reverse()[0]
+                    attachment["filePath"] = filePath
+                    recordingBar.audioRecorded(attachment)
+
+                    recordingBar.duration = duration
+                }
+            }
+            onErrorChanged: {
+                switch(errorCode) {
+                    case AudioRecorder.ResourceError:
+                        if (handleError) {
+                            timer.start()
+                        }
+                        break
+                    default:
+                }
+            }
+            codec: "audio/vorbis"
+            quality: AudioRecorder.VeryHighQuality
+        }
+    }
+
+    // WORKAROUND we can't trigger the dialog from the onErrorChanged signal.
+    Timer {
+        id: timer
+        interval: 1
+        onTriggered: {
+            Qt.inputMethod.hide()
+            messages.focus = false
+            PopupUtils.open(Qt.createComponent("Dialogs/NoMicrophonePermission.qml").createObject(messages))
+            handleError = false
+        }
+    }
+
+    TransparentButton {
+        id: recordingIcon
+        objectName: "recordingIcon"
+        iconPulsate: true
+        sideBySide: true
+        spacing: units.gu(1)
+        opacity: buttonOpacity
+
+        anchors {
+            left: parent.left
+            leftMargin: units.gu(2)
+            verticalCenter: parent.verticalCenter
+        }
+
+        focus: false
+
+        iconColor: "red"
+        iconName: "audio-input-microphone-symbolic"
+
+        textSize: FontUtils.sizeToPixels("x-small")
+        text: {
+            if (audioRecorder.recording) {
+                return DateUtils.formattedTime(audioRecorder.duration / 1000)
+            }
+            return DateUtils.formattedTime(0)
+        }
+    }
+
+    Label {
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+            left: recordingIcon.right
+            right: parent.right
+            topMargin: units.gu(1)
+            bottomMargin: units.gu(1)
+            leftMargin: units.gu(1)
+            rightMargin: units.gu(1)
+        }
+        opacity: buttonOpacity
+
+        text: i18n.tr("<<< Swipe to cancel")
+        verticalAlignment: Text.AlignVCenter
+        horizontalAlignment: Text.AlignHCenter
+    }
+}
+

=== modified file 'src/qml/CMakeLists.txt'
--- src/qml/CMakeLists.txt	2014-08-11 22:22:59 +0000
+++ src/qml/CMakeLists.txt	2016-02-05 00:58:50 +0000
@@ -17,3 +17,4 @@
 
 add_subdirectory(MMS)
 add_subdirectory(Dialogs)
+add_subdirectory(Stickers)

=== added file 'src/qml/ComposeBar.qml'
--- src/qml/ComposeBar.qml	1970-01-01 00:00:00 +0000
+++ src/qml/ComposeBar.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2012-2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import QtMultimedia 5.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3 as ListItem
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Content 0.1
+import Ubuntu.Telephony 0.1
+import messagingapp.private 0.1
+import "Stickers"
+
+Item {
+    id: composeBar
+
+    property bool showContents: true
+    property int maxHeight: textEntry.height + units.gu(2)
+    property variant attachments: []
+    property bool canSend: true
+    property alias text: messageTextArea.text
+    property bool audioAttached: attachments.count == 1 && attachments.get(0).contentType.toLowerCase().indexOf("audio/") > -1
+    // Audio QML component needs to process the recorded audio to find duration and AudioRecorder seems to erase duration after some events
+    property alias audioRecordedDuration: audioRecordingBar.duration
+    property alias recording: audioRecordingBar.recording
+    property bool oskEnabled: true
+
+    signal sendRequested(string text, var attachments)
+
+    // internal properties
+    property int _activeAttachmentIndex: -1
+    property int _defaultHeight: textEntry.height + attachmentPanel.height + stickersPicker.height + units.gu(2)
+
+    Component.onDestruction: {
+        composeBar.reset()
+    }
+
+    function forceFocus() {
+        messageTextArea.forceActiveFocus()
+    }
+
+    function reset() {
+        if (composeBar.audioAttached) {
+            FileOperations.remove(attachments.get(0).filePath)
+        }
+
+        textEntry.text = ""
+        attachments.clear()
+    }
+
+    function addAttachments(transfer) {
+        if (!transfer || !transfer.items) {
+            return
+        }
+
+        for (var i = 0; i < transfer.items.length; i++) {
+            if (String(transfer.items[i].text).length > 0) {
+                composeBar.text = String(transfer.items[i].text)
+                continue
+            }
+            var attachment = {}
+            if (!startsWith(String(transfer.items[i].url),"file://")) {
+                composeBar.text = String(transfer.items[i].url)
+                continue
+            }
+            var filePath = String(transfer.items[i].url).replace('file://', '')
+            // get only the basename
+            attachment["contentType"] = application.fileMimeType(filePath)
+            if (startsWith(attachment["contentType"], "text/vcard") ||
+                startsWith(attachment["contentType"], "text/x-vcard")) {
+                attachment["name"] = "contact.vcf"
+            } else {
+                attachment["name"] = filePath.split('/').reverse()[0]
+            }
+            attachment["filePath"] = filePath
+            attachments.append(attachment)
+        }
+    }
+
+    anchors.bottom: isSearching ? parent.bottom : keyboard.top
+    anchors.left: parent.left
+    anchors.right: parent.right
+    height: showContents ? Math.min(_defaultHeight, maxHeight) : 0
+    visible: showContents
+    clip: true
+
+    Behavior on height {
+        UbuntuNumberAnimation { }
+    }
+
+    MouseArea {
+        enabled: !composeBar.audioAttached
+        anchors.fill: parent
+        onClicked: {
+            forceFocus()
+        }
+    }
+
+    ListModel {
+        id: attachments
+    }
+
+    Component {
+        id: attachmentPopover
+
+        Popover {
+            id: popover
+            Column {
+                id: containerLayout
+                anchors {
+                    left: parent.left
+                    top: parent.top
+                    right: parent.right
+                }
+                ListItem.Standard {
+                    text: i18n.tr("Remove")
+                    onClicked: {
+                        attachments.remove(_activeAttachmentIndex)
+                        PopupUtils.close(popover)
+                    }
+                }
+            }
+            Component.onDestruction: _activeAttachmentIndex = -1
+        }
+    }
+
+    ListItem.ThinDivider {
+        anchors.top: parent.top
+    }
+
+    Row {
+        id: leftSideActions
+        opacity: {
+            if (composeBar.recording) {
+                // we need to fade the buttons in when dragging
+                return dragTarget.dragAmount
+            } else if (composeBar.audioAttached) {
+                return 0;
+            } else {
+                return 1
+            }
+        }
+
+        Behavior on opacity { UbuntuNumberAnimation {} }
+        visible: opacity > 0
+
+        width: childrenRect.width
+        height: childrenRect.height
+
+        anchors {
+            left: parent.left
+            leftMargin: units.gu(2)
+            verticalCenter: sendButton.verticalCenter
+        }
+        spacing: units.gu(2)
+
+        TransparentButton {
+            id: attachButton
+            objectName: "attachButton"
+            iconName: "add"
+            iconRotation: attachmentPanel.expanded ? 45 : 0
+            onClicked: {
+                attachmentPanel.expanded = !attachmentPanel.expanded
+                if (attachmentPanel.expanded) {
+                    stickersPicker.expanded = false
+                }
+            }
+        }
+
+        TransparentButton {
+            id: stickersButton
+            objectName: "stickersButton"
+            iconSource: (stickersPicker.expanded && oskEnabled) ? Qt.resolvedUrl("./assets/input-keyboard-symbolic.svg") :
+                                                                  Qt.resolvedUrl("./assets/face-smile-big-symbolic-2.svg")
+            visible: stickerPacksModel.count > 0
+            onClicked: {
+                if (!stickersPicker.expanded) {
+                    messageTextArea.focus = false
+                    stickersPicker.expanded = true
+                    attachmentPanel.expanded = false
+                } else {
+                    stickersPicker.expanded = false
+                    messageTextArea.forceActiveFocus()
+                }
+            }
+        }
+    }
+
+    AudioPlaybackBar {
+        id: audioPlaybackBar
+
+        anchors {
+            left: parent.left
+            right: audioRecordingBar.right
+            top: parent.top
+            bottom: attachmentPanel.top
+        }
+
+        source: composeBar.audioAttached ? attachments.get(0).filePath : ""
+        duration: audioRecordedDuration
+
+        opacity: composeBar.audioAttached ? 1.0 : 0.0
+        Behavior on opacity { UbuntuNumberAnimation {} }
+        visible: opacity > 0
+
+        onResetRequested: {
+            composeBar.reset()
+        }
+    }
+
+    AudioRecordingBar {
+        id: audioRecordingBar
+
+        anchors {
+            left: parent.left
+            right: dragTarget.left
+            top: parent.top
+            bottom: attachmentPanel.top
+        }
+
+        buttonOpacity: 1 - dragTarget.dragAmount
+
+        onAudioRecorded:  {
+            attachments.append(audio)
+        }
+    }
+
+    Item {
+        id: dragTarget
+
+        property real recordingX: recordButton.x
+        property real normalX: leftSideActions.x + leftSideActions.width
+        property real delta: recordingX - normalX
+        property real dragAmount: 1 - (x - normalX) / (delta > 0 ? delta : 0.0001)
+        x: (composeBar.recording || composeBar.audioAttached) ? recordingX : normalX
+        Behavior on x { UbuntuNumberAnimation { } }
+        width: 0
+    }
+
+    StyledItem {
+        id: textEntry
+        property alias text: messageTextArea.text
+        property alias inputMethodComposing: messageTextArea.inputMethodComposing
+        property int fullSize: composeBar.audioAttached ? messageTextArea.height : attachmentThumbnails.height + messageTextArea.height
+        style: Theme.createStyleComponent("TextAreaStyle.qml", textEntry)
+        anchors {
+            topMargin: units.gu(1)
+            top: parent.top
+            left: dragTarget.right
+            leftMargin: units.gu(2)
+            right: sendButton.left
+            rightMargin: units.gu(2)
+        }
+        height: attachments.count !== 0 && !composeBar.audioAttached ? fullSize + units.gu(1.5) : fullSize
+        onActiveFocusChanged: {
+            if(activeFocus) {
+                stickersPicker.expanded = false
+                messageTextArea.forceActiveFocus()
+            } else {
+                focus = false
+            }
+        }
+
+        onTextChanged: {
+            // in case there is audio attached and the user starts typing, we remove the attachment
+            // and continue the text message
+            if (text !== "" && composeBar.audioAttached) {
+                attachments.clear()
+            }
+        }
+
+        focus: false
+        opacity: composeBar.audioAttached ? 0.0 : 1.0
+        Behavior on opacity { UbuntuNumberAnimation {} }
+        MouseArea {
+            anchors.fill: parent
+            onClicked: forceFocus()
+        }
+        Flow {
+            id: attachmentThumbnails
+            spacing: units.gu(1)
+            anchors{
+                left: parent.left
+                right: parent.right
+                top: parent.top
+                topMargin: units.gu(1)
+                leftMargin: units.gu(1)
+                rightMargin: units.gu(1)
+            }
+            height: childrenRect.height
+
+            Repeater {
+                model: attachments
+                delegate: Loader {
+                    id: loader
+                    height: units.gu(8)
+                    source: {
+                        var contentType = getContentType(filePath)
+                        console.log(contentType)
+                        switch(contentType) {
+                        case ContentType.Contacts:
+                            return Qt.resolvedUrl("ThumbnailContact.qml")
+                        case ContentType.Pictures:
+                            return Qt.resolvedUrl("ThumbnailImage.qml")
+                        case ContentType.Videos:
+                            return Qt.resolvedUrl("ThumbnailVideo.qml")
+                        case ContentType.Unknown:
+                            return Qt.resolvedUrl("ThumbnailUnknown.qml")
+                        default:
+                            console.log("unknown content Type")
+                        }
+                    }
+                    onStatusChanged: {
+                        if (status == Loader.Ready) {
+                            item.index = index
+                            item.filePath = filePath
+                        }
+                    }
+
+                    Connections {
+                        target: loader.status == Loader.Ready ? loader.item : null
+                        ignoreUnknownSignals: true
+                        onPressAndHold: {
+                            Qt.inputMethod.hide()
+                            _activeAttachmentIndex = target.index
+                            PopupUtils.open(attachmentPopover, parent)
+                        }
+                    }
+                }
+            }
+        }
+
+        ListItem.ThinDivider {
+            id: divider
+
+            anchors {
+                left: parent.left
+                right: parent.right
+                top: attachmentThumbnails.bottom
+                margins: units.gu(0.5)
+            }
+            visible: attachments.count > 0
+        }
+
+        TextArea {
+            id: messageTextArea
+            objectName: "messageTextArea"
+            anchors {
+                top: attachments.count == 0 ? textEntry.top : attachmentThumbnails.bottom
+                left: parent.left
+                right: parent.right
+            }
+            // this value is to avoid letter being cut off
+            height: units.gu(4.3)
+            style: LocalTextAreaStyle {}
+            autoSize: true
+            maximumLineCount: attachments.count == 0 ? 8 : 4
+            placeholderText: {
+                if (telepathyHelper.ready) {
+                    var account = telepathyHelper.accountForId(presenceRequest.accountId)
+                    if (account && 
+                            (presenceRequest.type != PresenceRequest.PresenceTypeUnknown &&
+                             presenceRequest.type != PresenceRequest.PresenceTypeUnset) &&
+                             account.protocolInfo.serviceName !== "") {
+                        console.log(presenceRequest.accountId)
+                        console.log(presenceRequest.type)
+                        return account.protocolInfo.serviceName
+                    }
+                }
+                return i18n.tr("Write a message...")
+            }
+            focus: textEntry.focus
+            font.family: "Ubuntu"
+            font.pixelSize: FontUtils.sizeToPixels("medium")
+            color: "#5d5d5d"
+        }
+    }
+
+    AttachmentPanel {
+        id: attachmentPanel
+
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: textEntry.bottom
+            topMargin: units.gu(1)
+        }
+
+        Connections {
+            target: composeBar
+            onAudioAttachedChanged: {
+                if (composeBar.audioAttached) {
+                    attachmentPanel.expanded = false;
+                }
+            }
+        }
+
+        onAttachmentAvailable: {
+            attachments.append(attachment)
+            forceFocus()
+        }
+
+        onExpandedChanged: {
+            if (expanded && Qt.inputMethod.visible) {
+                attachmentPanel.forceActiveFocus()
+            }
+        }
+    }
+
+    Loader {
+        id: stickersPicker
+        property bool expanded: false
+        height: expanded ? item.height : 0
+        active: false
+        sourceComponent: stickersPickerComponent
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: textEntry.bottom
+        }
+        onExpandedChanged: {
+            if (expanded) {
+               stickersPicker.active = expanded
+            }
+            if (active) {
+                item.expanded = expanded
+            }
+        }
+    }
+
+    Component {
+        id: stickersPickerComponent
+        StickersPicker {
+            id: stickersPicker1
+
+            onExpandedChanged: {
+                if (expanded && Qt.inputMethod.visible) {
+                    stickersPicker1.forceActiveFocus()
+                }
+            }
+
+            onStickerSelected: {
+                if (!canSend) {
+                    // FIXME: show a dialog saying what we need to do to be able to send
+                    return
+                }
+
+                var attachment = {}
+                var filePath = String(path).replace('file://', '')
+                attachment["contentType"] = application.fileMimeType(filePath)
+                attachment["name"] = filePath.split('/').reverse()[0]
+                attachment["filePath"] = filePath
+
+                // we need to append the attachment to a ListModel, so create it dynamically
+                var attachments = Qt.createQmlObject("import QtQuick 2.0; ListModel { }", composeBar)
+                attachments.append(attachment)
+                composeBar.sendRequested("", attachments)
+                stickersPicker.expanded = false
+            }
+        }
+    }
+
+    TransparentButton {
+        id: sendButton
+        objectName: "sendButton"
+        anchors.verticalCenter: textEntry.verticalCenter
+        anchors.right: parent.right
+        anchors.rightMargin: units.gu(2)
+        iconSource: Qt.resolvedUrl("./assets/send.svg")
+        enabled: (canSend && (textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0))
+        opacity: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 ? 1.0 : 0.0
+        Behavior on opacity { UbuntuNumberAnimation {} }
+        visible: opacity > 0
+
+        onClicked: {
+            // make sure we flush everything we have prepared in the OSK preedit
+            Qt.inputMethod.commit();
+            if (textEntry.text == "" && attachments.count == 0) {
+                return
+            }
+
+            if (composeBar.audioAttached) {
+                textEntry.text = ""
+            }
+
+            composeBar.sendRequested(textEntry.text, attachments)
+        }
+    }
+
+    TransparentButton {
+        id: recordButton
+        objectName: "recordButton"
+
+        anchors {
+            verticalCenter: textEntry.verticalCenter
+            right: parent.right
+            rightMargin: units.gu(2)
+        }
+
+        opacity: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 ? 0.0 : 1.0
+        Behavior on opacity { UbuntuNumberAnimation {} }
+        visible: opacity > 0
+
+        iconColor: composeBar.recording ? "black" : "gray"
+        iconName: "audio-input-microphone-symbolic"
+
+        onPressed: audioRecordingBar.startRecording()
+        onReleased: {
+            audioRecordingBar.stopRecording()
+
+            // if dragged past the threshold, cancel
+            if (dragTarget.dragAmount >= 0.5) {
+                composeBar.reset()
+            }
+        }
+
+        // drag-to-cancel
+        drag.target: dragTarget
+        drag.axis: Drag.XAxis
+        drag.minimumX: (leftSideActions.x + leftSideActions.width)
+        drag.maximumX: recordButton.x
+
+    }
+}

=== renamed file 'src/qml/PictureImport.qml' => 'src/qml/ContentImport.qml'
--- src/qml/PictureImport.qml	2015-09-14 13:51:27 +0000
+++ src/qml/ContentImport.qml	2016-02-05 00:58:50 +0000
@@ -24,17 +24,33 @@
 
     property var importDialog: null
 
-    signal pictureReceived(string pictureUrl)
+    signal contentReceived(string contentUrl)
 
-    function requestNewPicture()
-    {
+    function requestContent(contentType) {
         if (!root.importDialog) {
             root.importDialog = PopupUtils.open(contentHubDialog, root)
+            root.importDialog.contentType = contentType
         } else {
             console.warn("Import dialog already running")
         }
     }
 
+    function requestPicture() {
+        requestContent(ContentHub.ContentType.Pictures)
+    }
+
+    function requestVideo() {
+        requestContent(ContentHub.ContentType.Videos)
+    }
+
+    function requestContact() {
+        requestContent(ContentHub.ContentType.Contacts)
+    }
+
+    function requestDocument() {
+        requestContent(ContentHub.ContentType.Documents)
+    }
+
     Component {
         id: contentHubDialog
 
@@ -42,6 +58,7 @@
             id: dialogue
 
             property alias activeTransfer: signalConnections.target
+            property alias contentType: peerPicker.contentType
 
             focus: true
             Rectangle {
@@ -76,7 +93,7 @@
                     if (dialogue.activeTransfer.state === ContentHub.ContentTransfer.Charged) {
                         dialogue.hide()
                         if (dialogue.activeTransfer.items.length > 0) {
-                            root.pictureReceived(dialogue.activeTransfer.items[0].url)
+                            root.contentReceived(dialogue.activeTransfer.items[0].url)
                         }
                     }
 

=== added file 'src/qml/DeliveryStatus.qml'
--- src/qml/DeliveryStatus.qml	1970-01-01 00:00:00 +0000
+++ src/qml/DeliveryStatus.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+import QtQuick 2.2
+import Ubuntu.History 0.1
+
+Image {
+    property int messageStatus: -1
+    enabled: true
+    height: enabled ? units.gu(1) : 0
+    width: enabled ? undefined : 0
+    fillMode: Image.PreserveAspectFit
+    source: {
+        if (!enabled) {
+            return ""
+        }
+        if (messageStatus == HistoryThreadModel.MessageStatusDelivered) {
+            return Qt.resolvedUrl("./assets/single_tick.svg")
+        } else if (messageStatus == HistoryThreadModel.MessageStatusRead) {
+            return Qt.resolvedUrl("./assets/double_tick.svg")
+        }
+        return ""
+    }
+}

=== added file 'src/qml/Dialogs/FileSizeWarningDialog.qml'
--- src/qml/Dialogs/FileSizeWarningDialog.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Dialogs/FileSizeWarningDialog.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * dialer-app 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; version 3.
+ *
+ * dialer-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+
+Component {
+    Dialog {
+        id: dialogue
+        title: i18n.tr("File size warning")
+        Column {
+            anchors.left: parent.left
+            anchors.right: parent.right
+            spacing: units.gu(2)
+
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                height: paintedHeight
+                verticalAlignment: Text.AlignVCenter
+                text: i18n.tr("You are trying to send big files (over 300Kb). Some operators might not be able to send it.")
+                wrapMode: Text.WordWrap
+            }
+            Row {
+                spacing: units.gu(4)
+                anchors.horizontalCenter: parent.horizontalCenter
+                Button {
+                    objectName: "okFileSizeWarningDialog"
+                    text: i18n.tr("Ok")
+                    color: UbuntuColors.orange
+                    onClicked: {
+                        PopupUtils.close(dialogue)
+                    }
+                }
+            }
+
+            Row {
+                CheckBox {
+                    id: dontAskAgainCheckbox
+                    checked: false
+                    onCheckedChanged: settings.messagesDontShowFileSizeWarning = checked
+                }
+                Label {
+                    text: i18n.tr("Don't show again")
+                    anchors.verticalCenter: dontAskAgainCheckbox.verticalCenter
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: dontAskAgainCheckbox.checked = !dontAskAgainCheckbox.checked
+                    }
+                }
+            }
+        }
+    }
+}

=== added file 'src/qml/Dialogs/NoMicrophonePermission.qml'
--- src/qml/Dialogs/NoMicrophonePermission.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Dialogs/NoMicrophonePermission.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * dialer-app 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; version 3.
+ *
+ * dialer-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+
+Component {
+    Dialog {
+        id: dialogue
+        title: i18n.tr("No permission to access microphone")
+        Column {
+            anchors.left: parent.left
+            anchors.right: parent.right
+            spacing: units.gu(2)
+
+            Label {
+                anchors.left: parent.left
+                anchors.right: parent.right
+                height: paintedHeight
+                verticalAlignment: Text.AlignVCenter
+                text: i18n.tr("Please grant access on <a href=\"system_settings\">System Settings &gt; Security &amp; Privacy</a>.")
+                wrapMode: Text.WordWrap
+                onLinkActivated: {
+                    PopupUtils.close(dialogue)
+                    Qt.openUrlExternally("settings:///system/security-privacy")
+                }
+            }
+            Row {
+                spacing: units.gu(4)
+                anchors.horizontalCenter: parent.horizontalCenter
+                Button {
+                    objectName: "okNoMicrophonePermission"
+                    text: i18n.tr("Ok")
+                    color: UbuntuColors.orange
+                    onClicked: {
+                        PopupUtils.close(dialogue)
+                    }
+                }
+            }
+        }
+    }
+}

=== added file 'src/qml/EmptyState.qml'
--- src/qml/EmptyState.qml	1970-01-01 00:00:00 +0000
+++ src/qml/EmptyState.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,38 @@
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+Item {
+    id: emptyStateScreen
+
+    property alias labelVisible: emptyStateLabel.visible
+
+    anchors {
+        left: parent.left
+        leftMargin: units.gu(6)
+        right: parent.right
+        rightMargin: units.gu(6)
+        verticalCenter: parent.verticalCenter
+    }
+    height: childrenRect.height
+    Icon {
+        id: emptyStateIcon
+        anchors.top: emptyStateScreen.top
+        anchors.horizontalCenter: parent.horizontalCenter
+        height: units.gu(5)
+        width: height
+        opacity: 0.3
+        name: "message"
+    }
+    Label {
+        id: emptyStateLabel
+        anchors.top: emptyStateIcon.bottom
+        anchors.topMargin: units.gu(2)
+        anchors.left: parent.left
+        anchors.right: parent.right
+        text: i18n.tr("Compose a new message by swiping up from the bottom of the screen.")
+        color: "#5d5d5d"
+        fontSize: "x-large"
+        wrapMode: Text.WordWrap
+        horizontalAlignment: Text.AlignHCenter
+    }
+}

=== added file 'src/qml/InputInfo.qml'
--- src/qml/InputInfo.qml	1970-01-01 00:00:00 +0000
+++ src/qml/InputInfo.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,24 @@
+import QtQuick 2.0
+//import Unity.InputInfo 0.1
+
+Item {
+    // FIXME: implement correctly without relying on unity private stuff
+    property bool hasMouse: mainView.dualPanel //miceModel.count > 0 || touchPadModel.count > 0
+    property bool hasKeyboard: false //keyboardsModel.count > 0
+
+    /*InputDeviceModel {
+        id: miceModel
+        deviceFilter: InputInfo.Mouse
+    }
+
+    InputDeviceModel {
+        id: touchPadModel
+        deviceFilter: InputInfo.TouchPad
+    }
+
+    InputDeviceModel {
+        id: keyboardsModel
+        deviceFilter: InputInfo.Keyboard
+    }*/
+}
+

=== modified file 'src/qml/KeyboardRectangle.qml'
--- src/qml/KeyboardRectangle.qml	2014-08-11 22:59:14 +0000
+++ src/qml/KeyboardRectangle.qml	2016-02-05 00:58:50 +0000
@@ -17,6 +17,7 @@
  */
 
 import QtQuick 2.2
+import GSettings 1.0
 
 Item {
     id: keyboardRect
@@ -25,6 +26,8 @@
     anchors.bottom: parent.bottom
     height: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
 
+    property bool oskEnabled: !gsettings.stayHidden
+
     function recursiveFindFocusedItem(parent) {
         if (parent.activeFocus) {
             return parent;
@@ -58,4 +61,9 @@
             }
         }
     }
+
+    GSettings {
+        id: gsettings
+        schema.id: "com.canonical.keyboard.maliit"
+    }
 }

=== removed file 'src/qml/LocalPageWithBottomEdge.qml'
--- src/qml/LocalPageWithBottomEdge.qml	2015-09-14 13:51:27 +0000
+++ src/qml/LocalPageWithBottomEdge.qml	1970-01-01 00:00:00 +0000
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2014 Canonical, Ltd.
- *
- * 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; version 3.
- *
- * 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/>.
- */
-
-/*
-    Example:
-
-    MainView {
-        objectName: "mainView"
-
-        applicationName: "com.ubuntu.developer.boiko.bottomedge"
-
-        width: units.gu(100)
-        height: units.gu(75)
-
-        Component {
-            id: pageComponent
-
-            PageWithBottomEdge {
-                id: mainPage
-                title: i18n.tr("Main Page")
-
-                Rectangle {
-                    anchors.fill: parent
-                    color: "white"
-                }
-
-                bottomEdgePageComponent: Page {
-                    title: "Contents"
-                    anchors.fill: parent
-                    //anchors.topMargin: contentsPage.flickable.contentY
-
-                    ListView {
-                        anchors.fill: parent
-                        model: 50
-                        delegate: ListItems.Standard {
-                            text: "One Content Item: " + index
-                        }
-                    }
-                }
-                bottomEdgeTitle: i18n.tr("Bottom edge action")
-            }
-        }
-
-        PageStack {
-            id: stack
-            Component.onCompleted: stack.push(pageComponent)
-        }
-    }
-
-*/
-
-import QtQuick 2.2
-import Ubuntu.Components 1.3
-
-Page {
-    id: page
-
-    property alias bottomEdgePageComponent: edgeLoader.sourceComponent
-    property alias bottomEdgePageSource: edgeLoader.source
-    property alias bottomEdgeTitle: tipLabel.text
-    property bool bottomEdgeEnabled: true
-    property int bottomEdgeExpandThreshold: page.height * 0.2
-    property int bottomEdgeExposedArea: bottomEdge.state !== "expanded" ? (page.height - bottomEdge.y - bottomEdge.tipHeight) : _areaWhenExpanded
-    property bool reloadBottomEdgePage: true
-
-    readonly property alias bottomEdgePage: edgeLoader.item
-    readonly property bool isReady: ((bottomEdge.y === 0) && bottomEdgePageLoaded && edgeLoader.item.active)
-    readonly property bool isCollapsed: (bottomEdge.y === page.height)
-    readonly property bool bottomEdgePageLoaded: (edgeLoader.status == Loader.Ready)
-    property var temporaryProperties: null
-
-    property bool _showEdgePageWhenReady: false
-    property int _areaWhenExpanded: 0
-
-    signal bottomEdgeReleased()
-    signal bottomEdgeDismissed()
-
-
-    function showBottomEdgePage(source, properties)
-    {
-        edgeLoader.setSource(source, properties)
-        temporaryProperties = properties
-        _showEdgePageWhenReady = true
-    }
-
-    function setBottomEdgePage(source, properties)
-    {
-        edgeLoader.setSource(source, properties)
-    }
-
-    function _pushPage()
-    {
-        if (edgeLoader.status === Loader.Ready) {
-            edgeLoader.item.active = true
-            page.pageStack.push(edgeLoader.item)
-            if (edgeLoader.item.flickable) {
-                edgeLoader.item.flickable.contentY = -page.header.height
-                edgeLoader.item.flickable.returnToBounds()
-            }
-            if (edgeLoader.item.ready)
-                edgeLoader.item.ready()
-        }
-    }
-
-
-    Component.onCompleted: {
-        // avoid a binding on the expanded height value
-        var expandedHeight = height;
-        _areaWhenExpanded = expandedHeight;
-    }
-
-    onActiveChanged: {
-        if (active) {
-            bottomEdge.state = "collapsed"
-        }
-    }
-
-    onBottomEdgePageLoadedChanged: {
-        if (_showEdgePageWhenReady && bottomEdgePageLoaded) {
-            bottomEdge.state = "expanded"
-            _showEdgePageWhenReady = false
-        }
-    }
-
-    InverseMouseArea {
-        anchors.fill: edgeLoader.item
-        sensingArea: mainView
-        enabled: !tip.hidden
-        onPressed: {
-            mouse.accepted = false
-            page.focus = false
-        }
-        z: 1
-    }
-
-    Rectangle {
-        id: bgVisual
-
-        color: "black"
-        anchors.fill: page
-        opacity: 0.7 * ((page.height - bottomEdge.y) / page.height)
-        z: 1
-    }
-
-    UbuntuShape {
-        id: tip
-        objectName: "bottomEdgeTip"
-
-        property bool hidden: (activeFocus === false) || ((bottomEdge.y - units.gu(1)) < tip.y)
-
-        enabled: mouseArea.enabled
-        anchors {
-            bottom: parent.bottom
-            horizontalCenter: bottomEdge.horizontalCenter
-            bottomMargin: hidden ? - height + units.gu(1) : -units.gu(1)
-            Behavior on bottomMargin {
-                SequentialAnimation {
-                    // wait some msecs in case of the focus change again, to avoid flickering
-                    PauseAnimation {
-                        duration: 300
-                    }
-                    UbuntuNumberAnimation {
-                        duration: UbuntuAnimation.SnapDuration
-                    }
-                }
-            }
-        }
-
-        z: 1
-        width: tipLabel.paintedWidth + units.gu(6)
-        height: bottomEdge.tipHeight + units.gu(1)
-        color: Theme.palette.normal.overlay
-        Label {
-            id: tipLabel
-
-            anchors {
-                top: parent.top
-                left: parent.left
-                right: parent.right
-            }
-            height: bottomEdge.tipHeight
-            verticalAlignment: Text.AlignVCenter
-            horizontalAlignment: Text.AlignHCenter
-            opacity: tip.hidden ? 0.0 : 1.0
-            Behavior on opacity {
-                UbuntuNumberAnimation {
-                    duration: UbuntuAnimation.SnapDuration
-                }
-            }
-        }
-    }
-
-    Rectangle {
-        id: shadow
-
-        anchors {
-            left: parent.left
-            right: parent.right
-            bottom: parent.bottom
-        }
-        height: units.gu(1)
-        z: 1
-        opacity: 0.0
-        gradient: Gradient {
-            GradientStop { position: 0.0; color: "transparent" }
-            GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.2) }
-        }
-    }
-
-    MouseArea {
-        id: mouseArea
-
-        property real previousY: -1
-        property string dragDirection: "None"
-
-        preventStealing: true
-        drag {
-            axis: Drag.YAxis
-            target: bottomEdge
-            minimumY: bottomEdge.pageStartY
-            maximumY: page.height
-        }
-        enabled: edgeLoader.status == Loader.Ready
-
-        anchors {
-            left: parent.left
-            right: parent.right
-            bottom: parent.bottom
-
-        }
-        height: bottomEdge.tipHeight
-        z: 1
-
-        onReleased: {
-            page.bottomEdgeReleased()
-            if ((dragDirection === "BottomToTop") &&
-                bottomEdge.y < (page.height - bottomEdgeExpandThreshold - bottomEdge.tipHeight)) {
-                bottomEdge.state = "expanded"
-            } else {
-                bottomEdge.state = "collapsed"
-            }
-            previousY = -1
-            dragDirection = "None"
-        }
-
-        onPressed: {
-            previousY = mouse.y
-            tip.forceActiveFocus()
-        }
-
-        onMouseYChanged: {
-            var yOffset = previousY - mouseY
-            // skip if was a small move
-            if (Math.abs(yOffset) <= units.gu(2)) {
-                return
-            }
-            previousY = mouseY
-            dragDirection = yOffset > 0 ? "BottomToTop" : "TopToBottom"
-        }
-    }
-
-    Rectangle {
-        id: bottomEdge
-        objectName: "bottomEdge"
-
-        readonly property int tipHeight: units.gu(3)
-        readonly property int pageStartY: 0
-
-        z: 1
-        color: Theme.palette.normal.background
-        clip: true
-        anchors {
-            left: parent.left
-            right: parent.right
-        }
-        height: page.height
-        y: height
-        visible: page.bottomEdgeEnabled && !page.isCollapsed
-        state: "collapsed"
-        states: [
-            State {
-                name: "collapsed"
-                PropertyChanges {
-                    target: bottomEdge
-                    y: bottomEdge.height
-                }
-            },
-            State {
-                name: "expanded"
-                PropertyChanges {
-                    target: bottomEdge
-                    y: bottomEdge.pageStartY
-                }
-            },
-            State {
-                name: "floating"
-                when: mouseArea.drag.active
-                PropertyChanges {
-                    target: shadow
-                    opacity: 1.0
-                }
-            }
-        ]
-
-        transitions: [
-            Transition {
-                to: "expanded"
-                SequentialAnimation {
-                    alwaysRunToEnd: true
-
-                    SmoothedAnimation {
-                        target: bottomEdge
-                        property: "y"
-                        duration: UbuntuAnimation.FastDuration
-                        easing.type: Easing.Linear
-                    }
-                    SmoothedAnimation {
-                        target: edgeLoader
-                        property: "anchors.topMargin"
-                        to: - units.gu(4)
-                        duration: UbuntuAnimation.FastDuration
-                        easing.type: Easing.Linear
-                    }
-                    SmoothedAnimation {
-                        target: edgeLoader
-                        property: "anchors.topMargin"
-                        to: 0
-                        duration: UbuntuAnimation.FastDuration
-                        easing: UbuntuAnimation.StandardEasing
-                    }
-                    ScriptAction {
-                        script: page._pushPage()
-                    }
-                }
-            },
-            Transition {
-                from: "expanded"
-                to: "collapsed"
-                SequentialAnimation {
-                    alwaysRunToEnd: true
-
-                    ScriptAction {
-                        script: {
-                            Qt.inputMethod.hide()
-                            edgeLoader.item.parent = edgeLoader
-                            edgeLoader.item.anchors.fill = edgeLoader
-                            edgeLoader.item.active = false
-                        }
-                    }
-                    SmoothedAnimation {
-                        target: bottomEdge
-                        property: "y"
-                        duration: UbuntuAnimation.SlowDuration
-                    }
-                    ScriptAction {
-                        script: {
-                            // destroy current bottom page
-                            if (page.reloadBottomEdgePage) {
-                                edgeLoader.active = false
-                                // remove properties from old instance
-                                if (edgeLoader.source !== "") {
-                                    var properties = {}
-                                    if (temporaryProperties !== null) {
-                                        properties = temporaryProperties
-                                        temporaryProperties = null
-                                    }
-
-                                    edgeLoader.setSource(edgeLoader.source, properties)
-                                }
-                                // tip will receive focus on page active true
-                            } else {
-                                tip.forceActiveFocus()
-                            }
-
-                            // notify
-                            page.bottomEdgeDismissed()
-
-                            edgeLoader.active = true
-                        }
-                    }
-                }
-            },
-            Transition {
-                from: "floating"
-                to: "collapsed"
-                SmoothedAnimation {
-                    target: bottomEdge
-                    property: "y"
-                    duration: UbuntuAnimation.FastDuration
-                }
-            }
-        ]
-
-        Loader {
-            id: edgeLoader
-
-            asynchronous: true
-            anchors.fill: parent
-            //WORKAROUND: The SDK move the page contents down to allocate space for the header we need to avoid that during the page dragging
-            Binding {
-                target: edgeLoader.status === Loader.Ready ? edgeLoader : null
-                property: "anchors.topMargin"
-                value:  edgeLoader.item && edgeLoader.item.flickable ? edgeLoader.item.flickable.contentY : 0
-                when: !page.isReady
-            }
-
-            onLoaded: {
-                tip.forceActiveFocus()
-                if (page.isReady && edgeLoader.item.active !== true) {
-                    page._pushPage()
-                }
-            }
-        }
-    }
-}

=== added file 'src/qml/MMS/MMSAudio.qml'
--- src/qml/MMS/MMSAudio.qml	1970-01-01 00:00:00 +0000
+++ src/qml/MMS/MMSAudio.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.2
+import QtMultimedia 5.0
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Themes.Ambiance 1.3
+import messagingapp.private 0.1
+import ".."
+import "../dateUtils.js" as DateUtils
+
+MMSBase {
+    id: audioDelegate
+
+    height: units.gu(5)
+    width: units.gu(28)
+    property string textColor: incoming ? "#5D5D5D" : "#FFFFFF"
+    swipeLocked: audioPlayer.playing
+
+    Rectangle {
+        id: shape
+        radius: units.gu(1)
+        smooth: true
+        anchors.top: parent.top
+        width: parent.width
+        height: parent.height
+        color: incoming ? "#FFFFFF" : "#3fb24f"
+        border.color: incoming ? "#888888" : "transparent"
+    }
+
+    Loader {
+        id: audioPlayer
+        readonly property bool playing: ready ? item.playing : false
+        readonly property bool paused: ready ? item.paused : false
+        readonly property bool stopped: ready ? item.stopped : false
+        readonly property int position: ready ? item.position : 0
+        readonly property int duration: ready ? item.duration : 0
+        readonly property bool ready: status == Loader.Ready
+        readonly property int playbackState: ready ? item.playbackState : Audio.StoppedState
+        property bool muted: false
+        property string source: ""
+        function play() {
+            var tmpFile = FileOperations.getTemporaryFile(".ogg")
+            if (FileOperations.link(attachment.filePath, tmpFile)) {
+                source = tmpFile;
+            } else {
+                console.log("MMSAudio: Failed to link", attachment.filePath, "to", tmpFile)
+                return
+            }
+
+            audioPlayer.active = true
+            item.play() 
+        }
+        function stop() {
+            item.stop()
+            audioPlayer.active = false
+            audioPlayer.muted = false
+        }
+        function pause() { item.pause() }
+        function seek(pos) { item.seek(pos) }
+        active: false
+        sourceComponent: audioPlayerComponent
+        Component.onDestruction: FileOperations.remove(source)
+    }
+
+    Component {
+        id: audioPlayerComponent
+        Audio {
+            id: audioPlayer1
+            objectName: "audioPlayer"
+            readonly property bool playing: audioPlayer1.playbackState == Audio.PlayingState
+            readonly property bool paused: audioPlayer1.playbackState == Audio.PausedState
+            readonly property bool stopped: audioPlayer1.playbackState == Audio.StoppedState
+            source: audioPlayer.source
+            muted: audioPlayer.muted
+        }
+    }
+
+
+    TransparentButton {
+        id: playButton
+        objectName: "playButton"
+
+        anchors {
+            left: parent.left
+            leftMargin: units.gu(1)
+            verticalCenter: shape.verticalCenter
+        }
+
+        spacing: units.gu(1)
+        sideBySide: true
+        iconColor: audioDelegate.textColor
+        iconName: audioPlayer.playing ? "media-playback-pause" : "media-playback-start"
+
+        textSize: FontUtils.sizeToPixels("x-small")
+        textColor: audioDelegate.textColor
+        text: {
+            if (audioPlayer.playing || audioPlayer.paused) {
+                return DateUtils.formattedTime(audioPlayer.position/ 1000)
+            }
+            if (audioPlayer.duration > 0) {
+                return DateUtils.formattedTime(audioPlayer.duration / 1000)
+            }
+            return ""
+        }
+
+        onClicked: {
+            if (audioPlayer.playing) {
+                audioPlayer.pause()
+            } else {
+                audioPlayer.play()
+            }
+        }
+    }
+
+    Slider {
+        id: slider
+        Connections {
+            target: audioPlayer
+            onDurationChanged: {
+                if (slider.maximumValue == 100) {
+                    slider.maximumValue = audioPlayer.duration
+                }
+            }
+        }
+        style: SliderStyle {
+            Component.onCompleted: thumb.visible = false
+            Connections {
+                target: audioPlayer
+                onPlaybackStateChanged: {
+                    thumb.visible = !audioPlayer.stopped
+                    if (!thumb.visible) {
+                        audioPlayer.seek(0)
+                    }
+                }
+            }
+        }
+        enabled: !audioPlayer.stopped
+        function formatValue(v) { return DateUtils.formattedTime(v/1000) }
+        anchors {
+            left: playButton.right
+            right: deliveryStatus.left
+            leftMargin: units.gu(1)
+            rightMargin: units.gu(2)
+            verticalCenter: shape.verticalCenter
+        }
+        height: units.gu(3)
+        minimumValue: 0.0
+        maximumValue: 100
+        value: audioPlayer.position
+        activeFocusOnPress: false
+        onPressedChanged: {
+            if (!pressed) {
+                if (audioPlayer.playing || audioPlayer.paused) {
+                    audioPlayer.seek(value)
+                } else {
+                    audioPlayer.muted = true
+                    // we only get the duration while playing
+                    audioPlayer.play()
+                    audioPlayer.pause()
+                    if (audioPlayer.duration == 100) {
+                        audioPlayer.seek((audioPlayer.duration*value)/100)
+                    } else {
+                        audioPlayer.seek(value)
+                    }
+                    audioPlayer.muted = false
+                    
+                }
+                value = Qt.binding(function(){ return audioPlayer.position})
+            }
+        }
+    }
+
+    DeliveryStatus {
+       id: deliveryStatus
+       messageStatus: textMessageStatus
+       enabled: showDeliveryStatus
+       anchors {
+           right: parent.right
+           rightMargin: units.gu(0.5)
+           verticalCenter: slider.verticalCenter
+       }
+    }
+}

=== modified file 'src/qml/MMS/MMSBase.qml'
--- src/qml/MMS/MMSBase.qml	2014-08-19 18:41:57 +0000
+++ src/qml/MMS/MMSBase.qml	2016-02-05 00:58:50 +0000
@@ -23,4 +23,6 @@
     property var attachment
     property string previewer
     property bool lastItem: false
+    property bool swipeLocked: false
+    property bool showDeliveryStatus: true
 }

=== modified file 'src/qml/MMS/MMSContact.qml'
--- src/qml/MMS/MMSContact.qml	2015-11-19 13:37:36 +0000
+++ src/qml/MMS/MMSContact.qml	2016-02-05 00:58:50 +0000
@@ -20,6 +20,7 @@
 import Ubuntu.Components 1.3
 import Ubuntu.Contacts 0.1
 import Ubuntu.History 0.1
+import ".."
 
 MMSBase {
     id: vcardDelegate
@@ -76,8 +77,8 @@
                 return "#3fb24f"
             }
         }
-        border.color: "#ACACAC"
-        radius: height * 0.1
+        border.color: incoming ? "#ACACAC" : "transparent"
+        radius: units.gu(1)
 
         ContactAvatar {
             id: avatar
@@ -132,4 +133,16 @@
 
         vCardUrl: attachment ? Qt.resolvedUrl(attachment.filePath) : ""
     }
+
+    DeliveryStatus {
+       id: deliveryStatus
+       messageStatus: textMessageStatus
+       enabled: showDeliveryStatus
+       anchors {
+           right: parent.right
+           rightMargin: units.gu(0.5)
+           bottom: parent.bottom
+           bottomMargin: units.gu(0.5)
+       }
+    }
 }

=== modified file 'src/qml/MMS/MMSImage.qml'
--- src/qml/MMS/MMSImage.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MMS/MMSImage.qml	2016-02-05 00:58:50 +0000
@@ -18,6 +18,7 @@
 
 import QtQuick 2.2
 import Ubuntu.Components 1.3
+import ".."
 
 MMSBase {
     id: imageDelegate
@@ -34,6 +35,7 @@
 
         image: Image {
             id: imageAttachment
+            objectName: "imageAttachment"
 
             fillMode: Image.PreserveAspectCrop
             smooth: true
@@ -83,4 +85,16 @@
             }
         }
     }
+
+    DeliveryStatus {
+       id: deliveryStatus
+       messageStatus: textMessageStatus
+       enabled: showDeliveryStatus
+       anchors {
+           right: parent.right
+           rightMargin: units.gu(0.5)
+           bottom: parent.bottom
+           bottomMargin: units.gu(0.5)
+       }
+    }
 }

=== modified file 'src/qml/MMS/MMSVideo.qml'
--- src/qml/MMS/MMSVideo.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MMS/MMSVideo.qml	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012, 2013, 2014 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -18,65 +18,95 @@
 
 import QtQuick 2.2
 import Ubuntu.Components 1.3
-import QtMultimedia 5.0
+import Ubuntu.Thumbnailer 0.1
 import ".."
 
 MMSBase {
     id: videoDelegate
 
     previewer: "MMS/PreviewerVideo.qml"
-    anchors.left: parent.left
-    anchors.right: parent.right
-    height: bubble.height + units.gu(1)
+    height: videoAttachment.height
+    width: videoAttachment.width
 
-    Item {
+    UbuntuShape {
         id: bubble
         anchors.top: parent.top
-        width: videoOutput.width + units.gu(3)
-        height: videoOutput.height + units.gu(2)
-
-        MediaPlayer {
-            id: video
-            autoLoad: true
-            autoPlay: false
-            source: attachment.filePath
-            onStatusChanged: {
-                if (status === MediaPlayer.Loaded) {
-                    // FIXME: there is no way to show the thumbnail
-                    video.play(); video.stop();
-
-                    // resize videoOutput, as width is not set
-                    // properly when using PreserveAspectFit
-                    if (videoOutput.height > units.gu(25)) {
-                        var percentageResized = units.gu(25)*100/(metaData.resolution.height)
-                        videoOutput.height = units.gu(25)
-                        videoOutput.width = (metaData.resolution.width*percentageResized)/100
-                    }
-                    if (videoOutput.width > units.gu(35)) {
-                        percentageResized = units.gu(35)*100/(metaData.resolution.width)
-                        videoOutput.width = units.gu(35)
-                        videoOutput.height = (metaData.resolution.height*percentageResized)/100
-                    }
+        width: image.width
+        height: image.height
+
+        image: Image {
+            id: videoAttachment
+            objectName: "videoAttachment"
+
+            fillMode: Image.PreserveAspectCrop
+            smooth: true
+            source: "image://thumbnailer/" + attachment.filePath
+            visible: false
+            asynchronous: true
+            height: Math.min(implicitHeight, units.gu(14))
+            width: Math.min(implicitWidth, units.gu(27))
+            cache: false
+
+            sourceSize.width: units.gu(27)
+            sourceSize.height: units.gu(27)
+
+            onStatusChanged:  {
+                if (status === Image.Error) {
+                    source = "image://theme/image-missing"
+                    width = 128
+                    height = 128
                 }
             }
         }
-        VideoOutput {
-            id: videoOutput
-            source: video
+
+        Icon {
+            objectName: "playbackStartIcon"
+            width: units.gu(3)
+            height: units.gu(3)
             anchors.centerIn: parent
-            anchors.horizontalCenterOffset: incoming ? units.gu(0.5) : -units.gu(0.5)
+            name: "media-playback-start"
+            color: "white"
+            opacity: 0.8
         }
 
         Rectangle {
-            color: "black"
-            opacity: 0.8
-            anchors.fill: videoOutput
-            Icon {
-                name: "media-playback-start"
-                width: units.gu(4)
-                height: units.gu(4)
-                anchors.centerIn: parent
+            visible: videoDelegate.lastItem
+            gradient: Gradient {
+                GradientStop { position: 0.0; color: "transparent" }
+                GradientStop { position: 1.0; color: "gray" }
+            }
+
+            anchors {
+                bottom: parent.bottom
+                left: parent.left
+                right: parent.right
+            }
+            height: units.gu(2)
+            radius: bubble.height * 0.1
+            Label {
+                anchors{
+                    left: parent.left
+                    bottom: parent.bottom
+                    leftMargin: incoming ? units.gu(2) : units.gu(1)
+                    bottomMargin: units.gu(0.5)
+                }
+                fontSize: "xx-small"
+                text: Qt.formatTime(timestamp).toLowerCase()
+                color: "white"
             }
         }
     }
+
+    DeliveryStatus {
+       id: deliveryStatus
+       messageStatus: textMessageStatus
+       enabled: showDeliveryStatus
+       anchors {
+           right: parent.right
+           rightMargin: units.gu(0.5)
+           bottom: parent.bottom
+           bottomMargin: units.gu(0.5)
+       }
+    }
+
 }

=== modified file 'src/qml/MMS/Previewer.qml'
--- src/qml/MMS/Previewer.qml	2015-11-17 14:39:21 +0000
+++ src/qml/MMS/Previewer.qml	2016-02-05 00:58:50 +0000
@@ -31,7 +31,7 @@
 
     function handleAttachment(filePath, handlerType)
     {
-        mainStack.push(picker, {"url": filePath, "handler": handlerType});
+        mainStack.addPageToCurrentColumn(previewerPage, picker, {"url": filePath, "handler": handlerType});
         actionTriggered()
     }
 
@@ -45,23 +45,29 @@
         previewerPage.handleAttachment(attachment.filePath, ContentHandler.Share)
     }
 
-    function backAction()
-    {
-        mainStack.pop()
+    header: PageHeader {
+        id: pageHeader
+
+        property alias leadingActions: leadingBar.actions
+        property alias trailingActions: trailingBar.actions
+
+        title: previewerPage.title
+        leadingActionBar {
+            id: leadingBar
+        }
+
+        trailingActionBar {
+            id: trailingBar
+        }
     }
 
-    title: ""
     state: "default"
     states: [
-        PageHeadState {
+        State {
+            id: defaultState
             name: "default"
-            head: previewerPage.head
-            backAction: Action {
-                iconName: "back"
-                text: i18n.tr("Back")
-                onTriggered: previewerPage.backAction()
-            }
-            actions: [
+
+            property list<QtObject> trailingActions: [
                 Action {
                     objectName: "saveButton"
                     text: i18n.tr("Save")
@@ -75,6 +81,11 @@
                     onTriggered: previewerPage.shareAttchment()
                 }
             ]
+            PropertyChanges {
+                target: pageHeader
+
+                trailingActions: defaultState.trailingActions
+            }
         }
     ]
 
@@ -109,11 +120,11 @@
 
             onPeerSelected: {
                 picker.curTransfer = peer.request();
-                mainStack.pop();
+                mainStack.removePage(picker);
                 if (picker.curTransfer.state === ContentTransfer.InProgress)
                     picker.__exportItems(picker.url);
             }
-            onCancelPressed: mainStack.pop();
+            onCancelPressed: mainStack.removePage(picker);
         }
 
         Connections {

=== modified file 'src/qml/MMS/PreviewerImage.qml'
--- src/qml/MMS/PreviewerImage.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MMS/PreviewerImage.qml	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012, 2013, 2014 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -19,18 +19,194 @@
 import QtQuick 2.2
 import Ubuntu.Components 1.3
 import Ubuntu.Content 0.1
+import Ubuntu.Thumbnailer 0.1
 import ".."
 
 Previewer {
+    id: imagePreviewer
+
+    // FIXME: this won't work correctly in windowed mode
+    Component.onCompleted: application.fullscreen = true
+    Component.onDestruction: application.fullscreen = false
+
+    Connections {
+        target: application
+        onFullscreenChanged: imagePreviewer.head.visible = !application.fullscreen
+    }
+
     title: i18n.tr("Image Preview")
     clip: true
-    Image {
-        anchors.centerIn: parent
+
+    Rectangle {
         anchors.fill: parent
-        fillMode: Image.PreserveAspectFit
-        source: attachment.filePath
-        cache: false
-        sourceSize.width: parent.width
-        sourceSize.height: parent.height
+        color: "black"
+    } 
+
+    Item {
+        id: imageItem
+        property bool pinchInProgress: zoomPinchArea.active
+        property size thumbSize: Qt.size(viewer.width * 1.05, viewer.height * 1.05)
+
+        onWidthChanged: {
+            // Only change thumbSize if width increases more than 5%
+            // that way we do not reload image for small resizes
+            if (width > thumbSize.width) {
+                thumbSize = Qt.size(width * 1.05, height * 1.05);
+            }
+        }
+
+        onHeightChanged: {
+            // Only change thumbSize if height increases more than 5%
+            // that way we do not reload image for small resizes
+            if (height > thumbSize.height) {
+                thumbSize = Qt.size(width * 1.05, height * 1.05);
+            }
+        }
+
+        function zoomIn(centerX, centerY, factor) {
+            flickable.scaleCenterX = centerX / (flickable.sizeScale * flickable.width);
+            flickable.scaleCenterY = centerY / (flickable.sizeScale * flickable.height);
+            flickable.sizeScale = factor;
+        }
+
+        function zoomOut() {
+            if (flickable.sizeScale != 1.0) {
+                flickable.scaleCenterX = flickable.contentX / flickable.width / (flickable.sizeScale - 1);
+                flickable.scaleCenterY = flickable.contentY / flickable.height / (flickable.sizeScale - 1);
+                flickable.sizeScale = 1.0;
+            }
+        }
+
+        width: parent.width
+        height: parent.height
+
+        ActivityIndicator {
+            objectName: "imageActivityIndicator"
+            anchors.centerIn: parent
+            visible: running
+            running: image.status != Image.Ready
+        }
+
+        PinchArea {
+            id: zoomPinchArea
+            anchors.fill: parent
+
+            property real initialZoom
+            property real maximumScale: 3.0
+            property real minimumZoom: 1.0
+            property real maximumZoom: 3.0
+            property bool active: false
+            property var center
+
+            onPinchStarted: {
+                active = true;
+                initialZoom = flickable.sizeScale;
+                center = zoomPinchArea.mapToItem(media, pinch.startCenter.x, pinch.startCenter.y);
+                imageItem.zoomIn(center.x, center.y, initialZoom);
+            }
+            onPinchUpdated: {
+                var zoomFactor = MathUtils.clamp(initialZoom * pinch.scale, minimumZoom, maximumZoom);
+                flickable.sizeScale = zoomFactor;
+            }
+            onPinchFinished: {
+                active = false;
+            }
+
+            Flickable {
+                id: flickable
+                anchors.fill: parent
+                contentWidth: media.width
+                contentHeight: media.height
+                contentX: (sizeScale - 1) * scaleCenterX * width
+                contentY: (sizeScale - 1) * scaleCenterY * height
+                interactive: !imageItem.pinchInProgress
+
+                property real sizeScale: 1.0
+                property real scaleCenterX: 0.0
+                property real scaleCenterY: 0.0
+
+                Behavior on sizeScale {
+                    enabled: !imageItem.pinchInProgress
+                    UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
+                }
+                Behavior on scaleCenterX {
+                    UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
+                }
+                Behavior on scaleCenterY {
+                    UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
+                }
+
+                Item {
+                    id: media
+
+                    width: flickable.width * flickable.sizeScale
+                    height: flickable.height * flickable.sizeScale
+
+                    Image {
+                        id: image
+                        objectName: "thumbnailImage"
+                        anchors.fill: parent
+                        asynchronous: true
+                        cache: false
+                        source: "image://thumbnailer/%1".arg(attachment.filePath.toString())
+                        sourceSize {
+                            width: imageItem.thumbSize.width
+                            height: imageItem.thumbSize.height
+                        }
+                        fillMode: Image.PreserveAspectFit
+                        opacity: status == Image.Ready ? 1.0 : 0.0
+                        Behavior on opacity { UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration} }
+                    }
+
+                    Image {
+                        id: highResolutionImage
+                        objectName: "highResolutionImage"
+                        anchors.fill: parent
+                        asynchronous: true
+                        cache: false
+                        source: flickable.sizeScale > 1.0 ? attachment.filePath : ""
+                        sourceSize {
+                            width: width
+                            height: height
+                        }
+                        fillMode: Image.PreserveAspectFit
+                    }
+                }
+
+                MouseArea {
+                    id: imageMouseArea
+                    anchors.fill: parent
+
+                    property bool clickAccepted: false
+
+                    onDoubleClicked: {
+                        if (imageMouseArea.clickAccepted) {
+                            return
+                        }
+
+                        clickTimer.stop()
+
+                        if (flickable.sizeScale < zoomPinchArea.maximumZoom) {
+                            imageItem.zoomIn(mouse.x, mouse.y, zoomPinchArea.maximumZoom);
+                        } else {
+                            imageItem.zoomOut();
+                        }
+                    }
+                    onClicked: {
+                        imageMouseArea.clickAccepted = false
+                        clickTimer.start()
+                    }
+                }
+
+                Timer {
+                    id: clickTimer
+                    interval: 200
+                    onTriggered: {
+                        imageMouseArea.clickAccepted = true
+                        application.fullscreen = !application.fullscreen
+                    }
+                }
+            }
+        }
     }
 }

=== modified file 'src/qml/MMS/PreviewerMultipleContacts.qml'
--- src/qml/MMS/PreviewerMultipleContacts.qml	2015-11-19 00:34:45 +0000
+++ src/qml/MMS/PreviewerMultipleContacts.qml	2016-02-05 00:58:50 +0000
@@ -42,7 +42,12 @@
     MultipleSelectionListView {
         id: contactList
 
-        anchors.fill: parent
+        anchors {
+            top: root.header.bottom
+            bottom: parent.bottom
+            left: parent.left
+            right: parent.right
+        }
         listModel: thumbnail.vcard.contacts
         listDelegate: ContactDelegate {
             id: contactDelegate
@@ -51,7 +56,7 @@
             property var contact: thumbnail.vcard.contacts[index]
 
             onClicked: {
-                mainStack.push(sigleContatPreviewer, {'contact': contact})
+                mainStack.addComponentToCurrentColumnSync(root, sigleContatPreviewer, {'contact': contact})
             }
         }
     }

=== modified file 'src/qml/MMS/PreviewerVideo.qml'
--- src/qml/MMS/PreviewerVideo.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MMS/PreviewerVideo.qml	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012, 2013, 2014 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -17,61 +17,153 @@
  */
 
 import QtQuick 2.2
+import QtMultimedia 5.0
 import Ubuntu.Components 1.3
-import QtMultimedia 5.0
+import Ubuntu.Content 0.1
+import Ubuntu.Thumbnailer 0.1
+import messagingapp.private 0.1
 import ".."
 
 Previewer {
+    id: videoPreviewer
+
     title: i18n.tr("Video Preview")
-    // This previewer implements only basic video controls: play/pause/rewind
-    onActionTriggered: video.pause()
-    MediaPlayer {
-        id: video
-        autoLoad: true
-        autoPlay: true
-        source: attachment.filePath
-    }
-    VideoOutput {
-        id: videoOutput
-        source: video
-        anchors.fill: parent
+    clip: true
+
+    // FIXME: this won't work correctly in windowed mode
+    Component.onCompleted: {
+        application.fullscreen = true
+        // Load Video player after toggling fullscreen to reduce flickering
+        videoLoader.active = true
+    }
+    Component.onDestruction: application.fullscreen = false
+
+    Connections {
+        target: application
+        onFullscreenChanged: {
+            videoPreviewer.head.visible = !application.fullscreen
+            toolbar.collapsed = application.fullscreen
+        }
+    }
+
+    Rectangle {
+        anchors.fill: parent
+        color: "black"
+    }
+
+    Loader {
+        id: videoLoader
+
+        anchors.fill: parent
+        active: false
+        sourceComponent: videoComponent
+
+        onStatusChanged: {
+            if (status == Loader.Ready) {
+                var tmpFile = FileOperations.getTemporaryFile(".mp4")
+                if (FileOperations.link(attachment.filePath, tmpFile)) {
+                    videoLoader.item.source = tmpFile
+                } else {
+                    console.log("MMSVideo: Failed to link", attachment.filePath, "to", tmpFile)
+                }
+            }
+        }
+
+        Component {
+            id: videoComponent
+
+            Item {
+                id: videoPlayer
+                objectName: "videoPlayer"
+
+                property alias source: player.source
+                property alias playbackState: player.playbackState
+
+                function play() { player.play() }
+                function pause() { player.pause() }
+                function stop() { player.stop() }
+ 
+                anchors.fill: parent
+
+                MediaPlayer {
+                    id: player
+                    autoPlay: true
+                }
+
+                VideoOutput {
+                    id: videoOutput
+                    anchors.fill: parent
+                    source: player
+                }
+            }
+        }
     }
 
     MouseArea {
-        id: playArea
-        anchors.fill: parent
-        onPressed: {
-            if (video.playbackState === MediaPlayer.PlayingState) {
-                video.pause()
-            }
+        anchors {
+            top: parent.top
+            bottom: toolbar.top
+            left: parent.left
+            right: parent.right
         }
+        onClicked: application.fullscreen = !application.fullscreen
     }
 
     Rectangle {
-        color: "black"
-        visible: video.playbackState !== MediaPlayer.PlayingState
+        id: toolbar
+        objectName: "toolbar"
+
+        property bool collapsed: false
+
+        anchors.bottom: parent.bottom
+
+        width: parent.width
+        height: collapsed ? 0 : units.gu(7)
+        Behavior on height { UbuntuNumberAnimation {} }
+
+        color: "gray"
         opacity: 0.8
-        anchors.fill: videoOutput
+
         Row {
-            anchors.centerIn: parent
+            anchors {
+                top: parent.top
+                bottom: parent.bottom
+                horizontalCenter: parent.horizontalCenter
+            }
+
+            spacing: units.gu(2)
+
             Icon {
-                name: "media-playback-pause"
-                width: units.gu(5)
-                height: units.gu(5)
+                anchors.verticalCenter: parent.verticalCenter
+                width: toolbar.collapsed ? 0 : units.gu(5)
+                height: width
+                Behavior on width { UbuntuNumberAnimation {} }
+                Behavior on height { UbuntuNumberAnimation {} }
+                name: videoLoader.item && videoLoader.item.playbackState == MediaPlayer.PlayingState ? "media-playback-pause" : "media-playback-start"
+                color: "white"
                 MouseArea {
                     anchors.fill: parent
-                    onClicked: video.play();
+                    onClicked: {
+                        if (videoLoader.item.playbackState == MediaPlayer.PlayingState) {
+                            videoLoader.item.pause()
+                        } else {
+                            videoLoader.item.play()
+                        }
+                    }
                 }
             }
             Icon {
-                name: "media-seek-backward"
-                width: units.gu(5)
-                height: units.gu(5)
+                anchors.verticalCenter: parent.verticalCenter
+                width: toolbar.collapsed ? 0 : units.gu(5)
+                height: width
+                Behavior on width { UbuntuNumberAnimation {} }
+                Behavior on height { UbuntuNumberAnimation {} }
+                name: "media-playback-stop"
+                color: "white"
                 MouseArea {
                     anchors.fill: parent
                     onClicked: {
-                        video.stop();
-                        video.play();
+                        videoLoader.item.stop()
                     }
                 }
             }

=== modified file 'src/qml/MMSDelegate.qml'
--- src/qml/MMSDelegate.qml	2015-11-23 19:51:16 +0000
+++ src/qml/MMSDelegate.qml	2016-02-05 00:58:50 +0000
@@ -27,6 +27,15 @@
     property var dataAttachments: []
     property var textAttachements: []
     property string messageText: ""
+    swipeLocked: {
+        for (var i=0; i < attachmentsView.children.length; i++) {
+            if (attachmentsView.children[i].item && !attachmentsView.children[i].item.swipeLocked) {
+                return false
+            }
+        }
+        return true
+    }
+
 
     function clicked(mouse)
     {
@@ -36,7 +45,7 @@
             var properties = {}
             properties["attachment"] = attachment.item.attachment
             properties["thumbnail"] = attachment.item
-            mainStack.push(Qt.resolvedUrl(attachment.item.previewer), properties)
+            mainStack.addFileToCurrentColumnSync(messages.basePage, Qt.resolvedUrl(attachment.item.previewer), properties)
         }
     }
 
@@ -82,17 +91,16 @@
             var attachment = attachments[i]
             if (startsWith(attachment.contentType, "text/plain") ) {
                 root.textAttachements.push(attachment)
+            } else if (startsWith(attachment.contentType, "audio/")) {
+                root.dataAttachments.push({"type": "audio",
+                                      "data": attachment,
+                                      "delegateSource": "MMS/MMSAudio.qml",
+                                    })
             } else if (startsWith(attachment.contentType, "image/")) {
                 root.dataAttachments.push({"type": "image",
                                       "data": attachment,
                                       "delegateSource": "MMS/MMSImage.qml",
                                     })
-            //} else if (startsWith(attachment.contentType, "video/")) {
-                        // TODO: implement proper video attachment support
-                        //                dataAttachments.push({type: "video",
-                        //                                  data: attachment,
-                        //                                  delegateSource: "MMS/MMSVideo.qml",
-                        //                                 })
             } else if (startsWith(attachment.contentType, "application/smil") ||
                        startsWith(attachment.contentType, "application/x-smil")) {
                 // smil files will always be ignored here
@@ -102,6 +110,11 @@
                                       "data": attachment,
                                       "delegateSource": "MMS/MMSContact.qml"
                                     })
+            } else if (startsWith(attachment.contentType, "video/")) {
+                root.dataAttachments.push({"type": "video",
+                                      "data": attachment,
+                                      "delegateSource": "MMS/MMSVideo.qml",
+                                    })
             } else {
                 root.dataAttachments.push({"type": "default",
                                       "data": attachment,
@@ -150,11 +163,6 @@
                             target: attachmentLoader
                             anchors.left: parent ? parent.left : undefined
                         }
-                        PropertyChanges {
-                            target: attachmentLoader
-                            anchors.leftMargin: units.gu(1)
-                            anchors.rightMargin: 0
-                        }
                     },
                     State {
                         when: !root.incoming
@@ -163,11 +171,6 @@
                             target: attachmentLoader
                             anchors.right: parent ? parent.right : undefined
                         }
-                        PropertyChanges {
-                            target: attachmentLoader
-                            anchors.leftMargin: 0
-                            anchors.rightMargin: units.gu(1)
-                        }
                     }
                 ]
                 source: modelData.delegateSource
@@ -221,7 +224,7 @@
                 target: bubbleLoader.item
                 property: "sender"
                 value: messageData.sender.alias !== "" ? messageData.sender.alias : messageData.senderId
-                when: participants.length > 1 && bubbleLoader.status === Loader.Ready && messageData.senderId !== "self"
+                when: messageData.participants.length > 1 && bubbleLoader.status === Loader.Ready && messageData.senderId !== "self"
             }
         }
     }

=== modified file 'src/qml/MMSMessageBubble.qml'
--- src/qml/MMSMessageBubble.qml	2014-08-27 16:05:37 +0000
+++ src/qml/MMSMessageBubble.qml	2016-02-05 00:58:50 +0000
@@ -25,6 +25,7 @@
     messageStatus: messageData.textMessageStatus
     messageIncoming: incoming
     accountName: accountLabel
+    showDeliveryStatus: true
 
     states: [
         State {

=== modified file 'src/qml/MainPage.qml'
--- src/qml/MainPage.qml	2015-10-30 13:21:59 +0000
+++ src/qml/MainPage.qml	2016-02-05 00:58:50 +0000
@@ -23,29 +23,27 @@
 import Ubuntu.History 0.1
 import "dateUtils.js" as DateUtils
 
-LocalPageWithBottomEdge {
+Page {
     id: mainPage
     property alias selectionMode: threadList.isInSelectionMode
     property bool searching: false
+    property bool isEmpty: threadCount == 0 && !threadModel.canFetchMore
     property alias threadCount: threadList.count
+    property alias displayedThreadIndex: threadList.currentIndex
+
+    property var _messagesPage: null
 
     function startSelection() {
         threadList.startSelection()
     }
 
-    state: selectionMode ? "select" : searching ? "search" : "default"
-    title: selectionMode ? " " : i18n.tr("Messages")
-    flickable: null
-
-    bottomEdgeEnabled: !selectionMode && !searching
-    bottomEdgeTitle: i18n.tr("+")
-    bottomEdgePageComponent: Messages { active: false }
-
     TextField {
         id: searchField
         objectName: "searchField"
         visible: mainPage.searching
         anchors {
+            top: parent.top
+            topMargin: units.gu(1)
             left: parent.left
             right: parent.right
             rightMargin: units.gu(2)
@@ -60,11 +58,30 @@
         }
     }
 
+    header: PageHeader {
+        id: pageHeader
+
+        property alias leadingActions: leadingBar.actions
+        property alias trailingActions: trailingBar.actions
+
+        title: i18n.tr("Messages")
+        flickable: threadList
+        leadingActionBar {
+            id: leadingBar
+        }
+
+        trailingActionBar {
+            id: trailingBar
+        }
+    }
+
     states: [
-        PageHeadState {
+        State {
+            id: defaultState
             name: "default"
-            head: mainPage.head
-            actions: [
+            when: !searching && !selectionMode
+
+            property list<QtObject> trailingActions: [
                 Action {
                     objectName: "searchAction"
                     iconName: "search"
@@ -77,38 +94,76 @@
                     objectName: "settingsAction"
                     text: i18n.tr("Settings")
                     iconName: "settings"
-                    onTriggered: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
+                    onTriggered: {
+                        emptyStack()
+                        pageStack.addFileToNextColumnSync(mainPage, Qt.resolvedUrl("SettingsPage.qml"))
+                    }
+                },
+                Action {
+                    objectName: "newMessageAction"
+                    text: i18n.tr("New message")
+                    iconName: "add"
+                    visible: dualPanel
+                    onTriggered: mainView.bottomEdge.commit()
                 }
+
             ]
+
+            PropertyChanges {
+                target: pageHeader
+                trailingActions: defaultState.trailingActions
+                leadingActions: []
+            }
         },
-        PageHeadState {
+        State {
+            id: searchState
             name: "search"
-            head: mainPage.head
-            backAction: Action {
-                objectName: "cancelSearch"
-                visible: mainPage.searching
-                iconName: "back"
-                text: i18n.tr("Cancel")
-                onTriggered: {
-                    searchField.text = ""
-                    mainPage.searching = false
+            when: searching
+
+            property list<QtObject> leadingActions: [
+                Action {
+                    objectName: "cancelSearch"
+                    visible: mainPage.searching
+                    iconName: "back"
+                    text: i18n.tr("Cancel")
+                    onTriggered: {
+                        searchField.text = ""
+                        mainPage.searching = false
+                    }
                 }
+            ]
+
+            PropertyChanges {
+                target: pageHeader
+                contents: searchField
+                leadingActions: searchState.leadingActions
+                trailingActions: []
             }
-            contents: searchField
         },
-        PageHeadState {
-            name: "select"
-            head: mainPage.head
-            backAction: Action {
-                objectName: "selectionModeCancelAction"
-                iconName: "back"
-                onTriggered: threadList.cancelSelection()
-            }
-            actions: [
+        State {
+            id: selectionState
+            name: "selection"
+            when: selectionMode
+
+            property list<QtObject> leadingActions: [
+                Action {
+                    objectName: "selectionModeCancelAction"
+                    iconName: "back"
+                    onTriggered: threadList.cancelSelection()
+                }
+            ]
+
+            property list<QtObject> trailingActions: [
                 Action {
                     objectName: "selectionModeSelectAllAction"
                     iconName: "select"
-                    onTriggered: threadList.selectAll()
+                    onTriggered: {
+                        if (threadList.selectedItems.count === threadList.count) {
+                            threadList.clearSelection()
+                        } else {
+                            threadList.selectAll()
+                        }
+                    }
                 },
                 Action {
                     objectName: "selectionModeDeleteAction"
@@ -117,39 +172,18 @@
                     onTriggered: threadList.endSelection()
                 }
             ]
+            PropertyChanges {
+                target: pageHeader
+                title: " "
+                leadingActions: selectionState.leadingActions
+                trailingActions: selectionState.trailingActions
+            }
         }
     ]
 
-    Item {
+    EmptyState {
         id: emptyStateScreen
-        anchors.left: parent.left
-        anchors.leftMargin: units.gu(6)
-        anchors.right: parent.right
-        anchors.rightMargin: units.gu(6)
-        height: childrenRect.height
-        anchors.verticalCenter: parent.verticalCenter
-        visible: threadCount == 0 && !threadModel.canFetchMore
-        Icon {
-            id: emptyStateIcon
-            anchors.top: emptyStateScreen.top
-            anchors.horizontalCenter: parent.horizontalCenter
-            height: units.gu(5)
-            width: height
-            opacity: 0.3
-            name: "message"
-        }
-        Label {
-            id: emptyStateLabel
-            anchors.top: emptyStateIcon.bottom
-            anchors.topMargin: units.gu(2)
-            anchors.left: parent.left
-            anchors.right: parent.right
-            text: i18n.tr("Compose a new message by swiping up from the bottom of the screen.")
-            color: "#5d5d5d"
-            fontSize: "x-large"
-            wrapMode: Text.WordWrap
-            horizontalAlignment: Text.AlignHCenter
-        }
+        visible: mainPage.isEmpty && !mainView.dualPanel
     }
 
     Component {
@@ -188,8 +222,18 @@
         clip: true
         cacheBuffer: threadList.height * 2
         section.property: "eventDate"
+        currentIndex: -1
         //spacing: searchField.text === "" ? units.gu(-2) : 0
         section.delegate: searching && searchField.text !== ""  ? null : sectionDelegate
+        header: ListItem.Standard {
+            id: newItem
+            height: mainView.bottomEdge.status === BottomEdge.Committed ? units.gu(10) : 0
+            text: i18n.tr("New message")
+            iconName: "message-new"
+            iconFrame: false
+            selected: true
+        }
+
         listDelegate: ThreadDelegate {
             id: threadDelegate
             // FIXME: find a better unique name
@@ -201,7 +245,15 @@
             }
             height: units.gu(8)
             selectionMode: threadList.isInSelectionMode
-            selected: threadList.isSelected(threadDelegate)
+            selected: {
+                if (selectionMode) {
+                    return threadList.isSelected(threadDelegate)
+                } else {
+                    // FIXME: there might be a better way of doing this
+                    return index === threadList.currentIndex && mainView.bottomEdge.status !== BottomEdge.Committed
+                }
+            }
+
             searchTerm: mainPage.searching ? searchField.text : ""
             onItemClicked: {
                 if (threadList.isInSelectionMode) {
@@ -218,10 +270,15 @@
                     }
                     properties["participantIds"] = participantIds
                     properties["participants"] = model.participants
+                    properties["presenceRequest"] = threadDelegate.presenceItem
                     if (displayedEvent != null) {
                         properties["scrollToEventId"] = displayedEvent.eventId
                     }
-                    mainStack.push(Qt.resolvedUrl("Messages.qml"), properties)
+                    emptyStack()
+                    mainStack.addComponentToNextColumnSync(mainPage, messagesWithBottomEdge, properties)
+
+                    // mark this item as current
+                    threadList.currentIndex = index
                 }
             }
             onItemPressAndHold: {
@@ -241,6 +298,13 @@
                 mainView.removeThreads(threadsToRemove);
             }
         }
+
+        Binding {
+            target: threadList
+            property: 'contentY'
+            value: -threadList.headerItem.height
+            when: mainView.composingNewMessage
+        }
     }
 
     KeyboardRectangle {
@@ -251,4 +315,12 @@
         flickableItem: threadList
         align: Qt.AlignTrailing
     }
+
+    Loader {
+        id: bottomEdgeLoader
+        active: !selectionMode && !searching && !mainView.dualPanel
+        sourceComponent: MessagingBottomEdge {
+            parent: mainPage
+        }
+    }
 }

=== modified file 'src/qml/MessageBubble.qml'
--- src/qml/MessageBubble.qml	2015-10-22 15:31:05 +0000
+++ src/qml/MessageBubble.qml	2016-02-05 00:58:50 +0000
@@ -24,7 +24,7 @@
 import "dateUtils.js" as DateUtils
 import "3rd_party/ba-linkify.js" as BaLinkify
 
-Rectangle {
+BorderImage {
     id: root
 
     property int messageStatus: -1
@@ -34,10 +34,15 @@
     property var messageTimeStamp
     property int maxDelegateWidth: units.gu(27)
     property string accountName
+    // FIXME for now we just display the delivery status if it's greater than Accepted
+    property bool showDeliveryStatus: false
+    property bool deliveryStatusAvailable: showDeliveryStatus && (statusDelivered || statusRead)
 
     readonly property bool error: (messageStatus === HistoryThreadModel.MessageStatusPermanentlyFailed)
     readonly property bool sending: (messageStatus === HistoryThreadModel.MessageStatusUnknown ||
                                      messageStatus === HistoryThreadModel.MessageStatusTemporarilyFailed) && !messageIncoming
+    readonly property bool statusDelivered: (messageStatus === HistoryThreadModel.MessageStatusDelivered)
+    readonly property bool statusRead: (messageStatus === HistoryThreadModel.MessageStatusRead)
 
     // XXXX: should be hoisted
     function getCountryCode() {
@@ -69,28 +74,33 @@
         return text
     }
 
-    color: {
+    property string color: {
         if (error) {
-            return "#fc4949"
+            return "red"
         } else if (sending) {
-            return "#b2b2b2"
+            return "grey"
         } else if (messageIncoming) {
-            return "#ffffff"
+            return "white"
         } else {
-            return "#3fb24f"
+            // FIXME: use blue for IM accounts
+            return "green"
         }
     }
-    border.color: "#ACACAC"
-
-    radius: units.gu(1)
-    height: senderName.height + senderName.anchors.topMargin + textLabel.height + textTimestamp.height + units.gu(1)
+    source: "assets/" + color + "_bubble.sci"
+    smooth: true
+
+    // FIXME: maybe we should put everything inside a container to make width and height calculation easier
+    height: senderName.height + senderName.anchors.topMargin + textLabel.height + border.bottom + units.gu(0.5) + (oneLine ? 0 : messageFooter.height + messageFooter.anchors.topMargin)
+
+    // if possible, put the timestamp and the delivery status in the same line as the text
+    property int oneLineWidth: textLabel.contentWidth + messageFooter.width
+    property bool oneLine: oneLineWidth <= units.gu(27)
     width:  Math.min(units.gu(27),
-                     Math.max(textLabel.contentWidth, textTimestamp.contentWidth, senderName.contentWidth))
+                     Math.max(oneLine ? oneLineWidth : textLabel.contentWidth,
+                              messageFooter.width,
+                              senderName.contentWidth,
+                              border.right + border.left - units.gu(3)))
             + units.gu(3)
-    anchors{
-        leftMargin:  units.gu(1)
-        rightMargin: units.gu(1)
-    }
 
     Label {
         id: senderName
@@ -126,73 +136,49 @@
         color: root.messageIncoming ? UbuntuColors.darkGrey : "white"
     }
 
-    Label {
-        id: textTimestamp
-        objectName: "messageDate"
+    Row {
+        id: messageFooter
+        width: childrenRect.width
+        spacing: units.gu(1)
 
-        anchors{
+        anchors {
             top: textLabel.bottom
-            topMargin: units.gu(0.5)
-            left: parent.left
-            leftMargin: units.gu(1)
+            topMargin: oneLine ? -textTimestamp.height : units.gu(0.5)
+            right: parent.right
+            rightMargin: units.gu(1)
         }
 
-        visible: !root.sending
-        height: units.gu(2)
-        width: visible ? maxDelegateWidth : 0
-        fontSize: "xx-small"
-        color: root.messageIncoming ? UbuntuColors.lightGrey : "white"
-        opacity: root.messageIncoming ? 1.0 : 0.8
-        elide: Text.ElideRight
-        text: {
-            if (messageTimeStamp === "")
-                return ""
-
-            var str = Qt.formatTime(messageTimeStamp, Qt.DefaultLocaleShortDate)
-            if (root.accountName.length === 0 || !root.messageIncoming) {
+        Label {
+            id: textTimestamp
+            objectName: "messageDate"
+
+            anchors.bottom: parent.bottom
+            visible: !root.sending
+            height: units.gu(2)
+            width: paintedWidth > maxDelegateWidth ? maxDelegateWidth : undefined
+            fontSize: "xx-small"
+            color: root.messageIncoming ? UbuntuColors.lightGrey : "white"
+            opacity: root.messageIncoming ? 1.0 : 0.8
+            elide: Text.ElideRight
+            verticalAlignment: Text.AlignVCenter
+            text: {
+                if (messageTimeStamp === "")
+                    return ""
+
+                var str = Qt.formatTime(messageTimeStamp, Qt.DefaultLocaleShortDate)
+                if (root.accountName.length === 0 || !root.messageIncoming) {
+                    return str
+                }
+                str += " @ %1".arg(root.accountName)
                 return str
             }
-            str += " @ %1".arg(root.accountName)
-            return str
-        }
-    }
-
-    ColoredImage {
-        id: bubbleArrow
-
-        source: Qt.resolvedUrl("./assets/conversation_bubble_arrow.png")
-        color: root.color
-        asynchronous: false
-        anchors {
-            bottom: parent.bottom
-            bottomMargin: units.gu(2)
-            leftMargin: -1
-            rightMargin: -1
-        }
-        width: units.gu(1)
-        height: units.gu(1.5)
-
-        states: [
-            State {
-                when: root.messageIncoming
-                name: "incoming"
-                AnchorChanges {
-                    target: bubbleArrow
-                    anchors.right: root.left
-                }
-            },
-            State {
-                when: !root.messageIncoming
-                name: "outgoing"
-                AnchorChanges {
-                    target: bubbleArrow
-                    anchors.left: root.right
-                }
-                PropertyChanges {
-                    target: bubbleArrow
-                    mirror: true
-                }
-            }
-        ]
+        }
+
+        DeliveryStatus {
+            id: deliveryStatus
+            messageStatus: messageStatus
+            enabled: deliveryStatusAvailable
+            anchors.verticalCenter: textTimestamp.verticalCenter
+        }
     }
 }

=== modified file 'src/qml/MessageDelegate.qml'
--- src/qml/MessageDelegate.qml	2014-08-28 21:54:12 +0000
+++ src/qml/MessageDelegate.qml	2016-02-05 00:58:50 +0000
@@ -26,6 +26,7 @@
     property bool incoming: (messageData && messageData.senderId !== "self")
     property string accountLabel: ""
     property var _lastItem
+    property bool swipeLocked: false
 
     function deleteMessage()
     {

=== modified file 'src/qml/MessageDelegateFactory.qml'
--- src/qml/MessageDelegateFactory.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MessageDelegateFactory.qml	2016-02-05 00:58:50 +0000
@@ -35,6 +35,8 @@
     signal resendMessage()
     signal copyMessage()
     signal showMessageDetails()
+    color: "transparent"
+    locked: loader.item.swipeLocked
 
     width: messageList.width
     leftSideAction: Action {

=== modified file 'src/qml/Messages.qml'
--- src/qml/Messages.qml	2015-12-07 17:55:17 +0000
+++ src/qml/Messages.qml	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012, 2013, 2014 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -25,6 +25,7 @@
 import Ubuntu.History 0.1
 import Ubuntu.Telephony 0.1
 import Ubuntu.Contacts 0.1
+import messagingapp.private 0.1
 
 import "dateUtils.js" as DateUtils
 
@@ -45,8 +46,6 @@
     // FIXME: MainView should provide if the view is in portait or landscape
     property int orientationAngle: Screen.angleBetween(Screen.primaryOrientation, Screen.orientation)
     property bool landscape: orientationAngle == 90 || orientationAngle == 270
-    property var activeTransfer: null
-    property int activeAttachmentIndex: -1
     property var sharedAttachmentsTransfer: []
     property alias contactWatcher: contactWatcherInternal
     property string text: ""
@@ -61,7 +60,23 @@
     property string firstParticipantId: participantIds.length > 0 ? participantIds[0] : ""
     property variant firstParticipant: participants.length > 0 ? participants[0] : null
     property var threads: []
+    property QtObject presenceRequest: presenceItem
     property var accountsModel: getAccountsModel()
+    property alias oskEnabled: keyboard.oskEnabled
+    property bool isReady: false
+    property string firstRecipientAlias: ((contactWatcher.isUnknown &&
+                                           contactWatcher.isInteractive) ||
+                                          contactWatcher.alias === "") ? contactWatcher.identifier : contactWatcher.alias
+
+
+    // When using this view from the bottom edge, we are not in the stack, so we need to push on top of the parent page
+    property var basePage: messages
+
+    property bool startedFromBottomEdge: false
+
+    signal ready
+    signal cancel
+
     function getAccountsModel() {
         var accounts = []
         // on new chat dialogs display all possible accounts
@@ -179,123 +194,44 @@
         return (!tmpAccount || tmpAccount.type == AccountEntry.PhoneAccount || tmpAccount.type == AccountEntry.MultimediaAccount)
     }
 
-    Connections {
-        target: telepathyHelper
-        onSetupReady: {
-            // force reevaluation
-            messages.account = Qt.binding(getCurrentAccount)
-            messages.phoneAccount = Qt.binding(isPhoneAccount)
-            head.sections.model = Qt.binding(getSectionsModel)
-            head.sections.selectedIndex = Qt.binding(getSelectedIndex)
-        }
-    }
-
-
-    Connections {
-        target: chatManager
-        onChatEntryCreated: {
-            // TODO: track using chatId and not participants
-            if (accountId == account.accountId && 
-                firstParticipant && participants[0] == firstParticipant.identifier) {
-                messages.chatEntry = chatEntry
-            }
-        }
-        onChatsChanged: {
-            for (var i in chatManager.chats) {
-                var chat = chatManager.chats[i]
-                // TODO: track using chatId and not participants
-                if (chat.account.accountId == account.accountId &&
-                    firstParticipant && chat.participants[0] == firstParticipant.identifier) {
-                    messages.chatEntry = chat
-                    return
-                }
-            }
-            messages.chatEntry = null
-        }
-    }
-
-    Timer {
-        id: typingTimer
-        interval: 6000
-        onTriggered: {
-            messages.userTyping = false;
-        }
-    }
-
-    Repeater {
-        model: messages.chatEntry ? messages.chatEntry.chatStates : null
-        Item {
-            function processChatState() {
-                if (modelData.state == ChatEntry.ChannelChatStateComposing) {
-                    messages.userTyping = true
-                    typingTimer.start()
-                } else {
-                    messages.userTyping = false
-                }
-            }
-            Component.onCompleted: processChatState()
-            Connections {
-                target: modelData
-                onStateChanged: processChatState()
-            }
-        }
-    }
-
-    MessagesHeader {
-        id: header
-        width: parent ? parent.width - units.gu(2) : undefined
-        height: units.gu(5)
-        title: messages.title
-        subtitle: {
-            if (userTyping) {
-                return i18n.tr("Typing..")
-            }
-            switch (presenceRequest.type) {
-            case PresenceRequest.PresenceTypeAvailable:
-                return i18n.tr("Online")
-            case PresenceRequest.PresenceTypeOffline:
-                return i18n.tr("Offline")
-            case PresenceRequest.PresenceTypeAway:
-                return i18n.tr("Away")
-            case PresenceRequest.PresenceTypeBusy:
-                return i18n.tr("Busy")
-            default:
-                return ""
-            }
-        }
-        visible: true
-    }
-
-    head {
-        id: head
-        sections.model: getSectionsModel()
-        sections.selectedIndex: getSelectedIndex()
-    }
-
-
-    function addAttachmentsToModel(transfer) {
-        for (var i in transfer.items) {
-            if (String(transfer.items[i].text).length > 0) {
-                messages.text = String(transfer.items[i].text)
-                continue
-            }
-            var attachment = {}
-            if (!startsWith(String(transfer.items[i].url),"file://")) {
-                messages.text = String(transfer.items[i].url)
-                continue
-            }
-            var filePath = String(transfer.items[i].url).replace('file://', '')
-            // get only the basename
-            attachment["contentType"] = application.fileMimeType(filePath)
-            if (startsWith(attachment["contentType"], "text/vcard") ||
-                startsWith(attachment["contentType"], "text/x-vcard")) {
-                attachment["name"] = "contact.vcf"
-            } else {
-                attachment["name"] = filePath.split('/').reverse()[0]
-            }
-            attachment["filePath"] = filePath
-            attachments.append(attachment)
-        }
+    function addNewThreadToFilter(newAccountId, participantIds) {
+        var newAccount = telepathyHelper.accountForId(newAccountId)
+        var matchType = HistoryThreadModel.MatchCaseSensitive
+        if (newAccount.type == AccountEntry.PhoneAccount || newAccount.type == AccountEntry.MultimediaAccount) {
+            matchType = HistoryThreadModel.MatchPhoneNumber
+        }
+
+        var thread = eventModel.threadForParticipants(newAccountId,
+                                           HistoryThreadModel.EventTypeText,
+                                           participantIds,
+                                           matchType,
+                                           true)
+        var threadId = thread.threadId
+
+        // dont change the participants list
+        if (messages.participants.length == 0) {
+            messages.participants = thread.participants
+            var ids = []
+            for (var i in messages.participants) {
+                ids.push(messages.participants[i].identifier)
+            }
+            messages.participantIds = ids;
+        }
+
+        var found = false;
+        for (var i in messages.threads) {
+            if (messages.threads[i].threadId == threadId && messages.threads[i].accountId == newAccountId) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            messages.threads.push({"accountId": newAccountId, "threadId": threadId})
+            reloadFilters = !reloadFilters
+        }
+
+        return thread
     }
 
     function sendMessageNetworkCheck() {
@@ -340,48 +276,21 @@
         }
 
         // create the new thread and update the threadId list
-        var thread = eventModel.threadForParticipants(messages.account.accountId,
-                                           HistoryThreadModel.EventTypeText,
-                                           participantIds,
-                                           messages.account.type == AccountEntry.PhoneAccount ? HistoryThreadModel.MatchPhoneNumber
-                                                                                              : HistoryThreadModel.MatchCaseSensitive,
-                                           true)
-        var threadId = thread.threadId
-
-        // dont change the participants list
-        if (messages.participants.length == 0) {
-            messages.participants = thread.participants
-            var ids = []
-            for (var i in messages.participants) {
-                ids.push(messages.participants[i].identifier)
-            }
-            messages.participantIds = ids;
-        }
-
-        var found = false;
-        for (var i in messages.threads) {
-            if (messages.threads[i].threadId == threadId && messages.threads[i].accountId == messages.account.accountId) {
-                found = true;
-                break;
-            }
-        }
-        if (!found) {
-            messages.threads.push({"accountId": messages.account.accountId, "threadId": threadId})
-            reloadFilters = !reloadFilters
-        }
+        var thread = addNewThreadToFilter(messages.account.accountId, participantIds)
+
         for (var i=0; i < eventModel.count; i++) {
             var event = eventModel.get(i)
             if (event.senderId == "self" && event.accountId != messages.account.accountId) {
                 var tmpAccount = telepathyHelper.accountForId(event.accountId)
                 if (!tmpAccount || (tmpAccount.type == AccountEntry.MultimediaAccount && messages.account.type == AccountEntry.PhoneAccount)) {
-                    // we don't add the information event if the last outgoing message 
+                    // we don't add the information event if the last outgoing message
                     // was a fallback to a multimedia service
                     break;
                 }
                 // if the last outgoing message used a different accountId, add an
                 // information event and quit the loop
                 eventModel.writeTextInformationEvent(messages.account.accountId,
-                                                     threadId,
+                                                     thread.threadId,
                                                      participantIds,
                                                      "")
                 break;
@@ -399,7 +308,7 @@
             var timestamp = new Date()
             var tmpEventId = timestamp.toISOString()
             event["accountId"] = messages.account.accountId
-            event["threadId"] = threadId
+            event["threadId"] = thread.threadId
             event["eventId"] =  tmpEventId
             event["type"] = HistoryEventModel.MessageTypeText
             event["participants"] = thread.participants
@@ -417,7 +326,7 @@
                     var attachment = {}
                     var item = attachments[i]
                     attachment["accountId"] = messages.account.accountId
-                    attachment["threadId"] = threadId
+                    attachment["threadId"] = thread.threadId
                     attachment["eventId"] = tmpEventId
                     attachment["attachmentId"] = item[0]
                     attachment["contentType"] = item[1]
@@ -436,10 +345,14 @@
             var isSelfContactKnown = account.selfContactId != ""
             if (isMmsGroupChat && !isSelfContactKnown) {
                 // TODO: inform the user to enter the phone number of the selected sim card manually
-                // and use it in the telepathy-ofono account as selfContactId. 
-                return
-            }
-            chatManager.sendMessage(messages.account.accountId, participantIds, text, attachments, properties)
+                // and use it in the telepathy-ofono account as selfContactId.
+                return false
+            }
+            var fallbackAccountId = chatManager.sendMessage(messages.account.accountId, participantIds, text, attachments, properties)
+            // create the new thread and update the threadId list
+            if (fallbackAccountId != messages.account.accountId) {
+                addNewThreadToFilter(fallbackAccountId, participantIds)
+            }
         }
 
         // FIXME: soon it won't be just about SIM cards, so the dialogs need updating
@@ -455,120 +368,6 @@
         return true
     }
 
-    PresenceRequest {
-        id: presenceRequest
-        accountId: {
-            // if this is a regular sms chat, try requesting the presence on
-            // a multimedia account
-            if (!account) {
-                return ""
-            }
-            if (account.type == AccountEntry.PhoneAccount) {
-                for (var i in telepathyHelper.accounts) {
-                    var tmpAccount = telepathyHelper.accounts[i]
-                    if (tmpAccount.type == AccountEntry.MultimediaAccount) {
-                        return tmpAccount.accountId
-                    }
-                }
-                return ""
-            }
-            return account.accountId
-        }
-        // we just request presence on 1-1 chats
-        identifier: participants.length == 1 ? participants[0].identifier : ""
-    }
-
-    // this is necessary to automatically update the view when the
-    // default account changes in system settings
-    Connections {
-        target: mainView
-        onAccountChanged: {
-            if (!messages.phoneAccount) {
-                return
-            }
-            messages.account = mainView.account
-        }
-    }
-
-    ActivityIndicator {
-        id: activityIndicator
-        anchors {
-            verticalCenter: parent.verticalCenter
-            horizontalCenter: parent.horizontalCenter
-        }
-        running: isSearching
-        visible: running
-    }
-
-    ListModel {
-        id: attachments
-    }
-
-    PictureImport {
-        id: pictureImporter
-
-        onPictureReceived: {
-            var attachment = {}
-            var filePath = String(pictureUrl).replace('file://', '')
-            attachment["contentType"] = application.fileMimeType(filePath)
-            attachment["name"] = filePath.split('/').reverse()[0]
-            attachment["filePath"] = filePath
-            attachments.append(attachment)
-            textEntry.forceActiveFocus()
-        }
-    }
-
-    flickable: null
-
-    property bool isReady: false
-    signal ready
-    onReady: {
-        isReady = true
-        if (participants.length === 0 && keyboardFocus)
-            multiRecipient.forceFocus()
-    }
-
-    property string firstRecipientAlias: ((contactWatcher.isUnknown &&
-                                           contactWatcher.isInteractive) ||
-                                          contactWatcher.alias === "") ? contactWatcher.identifier : contactWatcher.alias
-    title: {
-        if (selectionMode || participants.length == 0) {
-            return " "
-        }
-
-        if (landscape) {
-            return ""
-        }
-        if (participants.length > 0) {
-            if (participants.length == 1) {
-                return firstRecipientAlias
-            } else {
-                // TRANSLATORS: %1 refers to the number of participants in a group chat
-                return i18n.tr("Group (%1)").arg(participants.length)
-            }
-        }
-        return i18n.tr("New Message")
-    }
-
-    Component.onCompleted: {
-        if (messages.accountId !== "") {
-            var account = telepathyHelper.accountForId(messages.accountId)
-            if (account && account.type == AccountEntry.MultimediaAccount) {
-                // fallback the first available phone account 
-                if (telepathyHelper.phoneAccounts.length > 0) {
-                    messages.accountId = telepathyHelper.phoneAccounts[0].accountId
-                }
-            }
-        }
-        addAttachmentsToModel(sharedAttachmentsTransfer)
-    }
-
-    onActiveChanged: {
-        if (active && (eventModel.count > 0)){
-            swipeItemDemo.enable()
-        }
-    }
-
     function updateFilters(accounts, participants, reload, threads) {
         if (participants.length == 0 || accounts.length == 0) {
             return null
@@ -584,7 +383,7 @@
             }
             return Qt.createQmlObject(componentUnion.arg(componentFilters), eventModel)
         }
- 
+
         var filterAccounts = []
 
         if (messages.accountsModel.length == 1 && messages.accountsModel[0].type == AccountEntry.GenericAccount) {
@@ -596,9 +395,9 @@
                     filterAccounts.push(account)
                 }
             }
-       }
+        }
 
-       for (var i in filterAccounts) {
+        for (var i in filterAccounts) {
             var account = filterAccounts[i];
             var filterValue = eventModel.threadIdForParticipants(account.accountId,
                                                                  HistoryThreadModel.EventTypeText,
@@ -628,8 +427,356 @@
         return eventModel.markEventAsRead(accountId, threadId, eventId, type);
     }
 
+    header: PageHeader {
+        id: pageHeader
+
+        property alias leadingActions: leadingBar.actions
+        property alias trailingActions: trailingBar.actions
+
+        property list<QtObject> bottomEdgeLeadingActions: [
+            Action {
+                id: backAction
+
+                objectName: "cancel"
+                name: "cancel"
+                text: i18n.tr("Cancel")
+                iconName: "down"
+                shortcut: "Esc"
+                onTriggered: {
+                    messages.cancel()
+                }
+            }
+        ]
+
+        property list<QtObject> singlePanelLeadingActions: [
+            Action {
+                id: singlePanelBackAction
+                objectName: "back"
+                name: "cancel"
+                text: i18n.tr("Cancel")
+                iconName: "back"
+                shortcut: "Esc"
+                onTriggered: {
+                    // emptyStack will make sure the page gets removed.
+                    mainView.emptyStack()
+                }
+            }
+        ]
+
+        title: {
+            if (landscape) {
+                return ""
+            }
+
+            if (participants.length == 1) {
+                return firstRecipientAlias
+            }
+
+            return i18n.tr("New Message")
+        }
+        flickable: null
+
+        Sections {
+            id: sections
+            anchors {
+                left: parent.left
+                leftMargin: units.gu(2)
+                bottom: parent.bottom
+            }
+            model: getSectionsModel()
+            selectedIndex: getSelectedIndex()
+            onSelectedIndexChanged: {
+                if (selectedIndex >= 0) {
+                    messages.account = messages.accountsModel[selectedIndex]
+                }
+            }
+        }
+
+        extension: sections.model.length > 1 ? sections : null
+
+        leadingActionBar {
+            id: leadingBar
+
+            states: [
+                State {
+                    name: "bottomEdgeBack"
+                    when: startedFromBottomEdge
+                    PropertyChanges {
+                        target: leadingBar
+                        actions: pageHeader.bottomEdgeLeadingActions
+                    }
+                },
+                State {
+                    name: "singlePanelBack"
+                    when: !mainView.dualPanel && !startedFromBottomEdge
+                    PropertyChanges {
+                        target: leadingBar
+                        actions: pageHeader.singlePanelLeadingActions
+                    }
+                }
+
+            ]
+        }
+
+        trailingActionBar {
+            id: trailingBar
+        }
+    }
+
+    states: [
+        State {
+            id: selectionState
+            name: "selection"
+            when: selectionMode
+
+            property list<QtObject> leadingActions: [
+                Action {
+                    objectName: "selectionModeCancelAction"
+                    iconName: "back"
+                    onTriggered: messageList.cancelSelection()
+                }
+            ]
+
+            property list<QtObject> trailingActions: [
+                Action {
+                    objectName: "selectionModeSelectAllAction"
+                    iconName: "select"
+                    onTriggered: {
+                        if (messageList.selectedItems.count === messageList.count) {
+                            messageList.clearSelection()
+                        } else {
+                            messageList.selectAll()
+                        }
+                    }
+                },
+                Action {
+                    objectName: "selectionModeDeleteAction"
+                    enabled: messageList.selectedItems.count > 0
+                    iconName: "delete"
+                    onTriggered: messageList.endSelection()
+                }
+            ]
+
+            PropertyChanges {
+                target: pageHeader
+                title: " "
+                leadingActions: selectionState.leadingActions
+                trailingActions: selectionState.trailingActions
+            }
+        },
+        State {
+            id: groupChatState
+            name: "groupChat"
+            when: groupChat
+
+            property list<QtObject> trailingActions: [
+                Action {
+                    objectName: "groupChatAction"
+                    iconName: "contact-group"
+                    onTriggered: PopupUtils.open(participantsPopover, pageHeader)
+                }
+            ]
+
+            PropertyChanges {
+                target: pageHeader
+                // TRANSLATORS: %1 refers to the number of participants in a group chat
+                title: i18n.tr("Group (%1)").arg(participants.length)
+                contents: headerContents
+                trailingActions: groupChatState.trailingActions
+            }
+        },
+        State {
+            id: unknownContactState
+            name: "unknownContact"
+            when: participants.length == 1 && contactWatcher.isUnknown
+
+            property list<QtObject> trailingActions: [
+                Action {
+                    objectName: "contactCallAction"
+                    visible: participants.length == 1 && contactWatcher.interactive
+                    iconName: "call-start"
+                    text: i18n.tr("Call")
+                    onTriggered: {
+                        Qt.inputMethod.hide()
+                        // FIXME: support other things than just phone numbers
+                        Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
+                    }
+                },
+                Action {
+                    objectName: "addContactAction"
+                    visible: contactWatcher.isUnknown && participants.length == 1 && contactWatcher.interactive
+                    iconName: "contact-new"
+                    text: i18n.tr("Add")
+                    onTriggered: {
+                        Qt.inputMethod.hide()
+                        // FIXME: support other things than just phone numbers
+                        mainView.addPhoneToContact(messages, "", contactWatcher.identifier, null, null)
+                    }
+                }
+            ]
+            PropertyChanges {
+                target: pageHeader
+                contents: headerContents
+                trailingActions: unknownContactState.trailingActions
+            }
+        },
+        State {
+            id: newMessageState
+            name: "newMessage"
+            when: participants.length === 0
+
+            property list<QtObject> trailingActions: [
+                Action {
+                    objectName: "contactList"
+                    iconName: "contact"
+                    onTriggered: {
+                        Qt.inputMethod.hide()
+                        mainStack.addFileToCurrentColumnSync(messages.basePage,  Qt.resolvedUrl("NewRecipientPage.qml"), {"multiRecipient": multiRecipient})
+                    }
+                }
+
+            ]
+
+            property Item contents: MultiRecipientInput {
+                id: multiRecipient
+                objectName: "multiRecipient"
+                enabled: visible
+                anchors {
+                    left: parent ? parent.left : undefined
+                    right: parent ? parent.right : undefined
+                    rightMargin: units.gu(2)
+                    top: parent ? parent.top: undefined
+                    topMargin: units.gu(1)
+                }
+                focus: true
+
+                Connections {
+                    target: mainView.bottomEdge
+                    onStatusChanged: {
+                        if (mainView.bottomEdge.status === BottomEdge.Committed) {
+                            multiRecipient.forceFocus()
+                        }
+                    }
+                }
+            }
+
+            PropertyChanges {
+                target: pageHeader
+                title: " "
+                trailingActions: newMessageState.trailingActions
+                contents: newMessageState.contents
+            }
+        },
+        State {
+            id: knownContactState
+            name: "knownContact"
+            when: participants.length == 1 && !contactWatcher.isUnknown
+            property list<QtObject> trailingActions: [
+                Action {
+                    objectName: "contactCallKnownAction"
+                    visible: participants.length == 1 && messages.phoneAccount
+                    iconName: "call-start"
+                    text: i18n.tr("Call")
+                    onTriggered: {
+                        Qt.inputMethod.hide()
+                        // FIXME: support other things than just phone numbers
+                        Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
+                    }
+                },
+                Action {
+                    objectName: "contactProfileAction"
+                    visible: !contactWatcher.isUnknown && participants.length == 1 && messages.phoneAccount
+                    iconSource: "image://theme/contact"
+                    text: i18n.tr("Contact")
+                    onTriggered: {
+                        mainView.showContactDetails(messages.basePage, contactWatcher.contactId, null, null)
+                    }
+                }
+            ]
+            PropertyChanges {
+                target: pageHeader
+                contents: headerContents
+                trailingActions: knownContactState.trailingActions
+            }
+        }
+    ]
+
+    Component.onCompleted: {
+        if (messages.accountId !== "") {
+            var account = telepathyHelper.accountForId(messages.accountId)
+            if (account && account.type == AccountEntry.MultimediaAccount) {
+                // fallback the first available phone account
+                if (telepathyHelper.phoneAccounts.length > 0) {
+                    messages.accountId = telepathyHelper.phoneAccounts[0].accountId
+                }
+            }
+        }
+        composeBar.addAttachments(sharedAttachmentsTransfer)
+    }
+
+    Component.onDestruction: {
+        if (!mainView.dualPanel && !startedFromBottomEdge) {
+            mainPage.displayedThreadIndex = -1
+        }
+    }
+
+    onReady: {
+        isReady = true
+        if (participants.length === 0 && keyboardFocus)
+            multiRecipient.forceFocus()
+    }
+
+    onActiveChanged: {
+        if (active && (eventModel.count > 0)){
+            swipeItemDemo.enable()
+        }
+    }
+
+    Connections {
+        target: telepathyHelper
+        onSetupReady: {
+            // force reevaluation
+            messages.account = Qt.binding(getCurrentAccount)
+            messages.phoneAccount = Qt.binding(isPhoneAccount)
+            head.sections.model = Qt.binding(getSectionsModel)
+            head.sections.selectedIndex = Qt.binding(getSelectedIndex)
+        }
+    }
+
+    Connections {
+        target: chatManager
+        onChatEntryCreated: {
+            // TODO: track using chatId and not participants
+            if (accountId == account.accountId &&
+                firstParticipant && participants[0] == firstParticipant.identifier) {
+                messages.chatEntry = chatEntry
+            }
+        }
+        onChatsChanged: {
+            for (var i in chatManager.chats) {
+                var chat = chatManager.chats[i]
+                // TODO: track using chatId and not participants
+                if (chat.account.accountId == account.accountId &&
+                    firstParticipant && chat.participants[0] == firstParticipant.identifier) {
+                    messages.chatEntry = chat
+                    return
+                }
+            }
+            messages.chatEntry = null
+        }
+    }
+
+    // this is necessary to automatically update the view when the
+    // default account changes in system settings
     Connections {
         target: mainView
+        onAccountChanged: {
+            if (!messages.phoneAccount) {
+                return
+            }
+            messages.account = mainView.account
+        }
+
         onApplicationActiveChanged: {
             if (mainView.applicationActive) {
                 for (var i in pendingEventsToMarkAsRead) {
@@ -641,28 +788,89 @@
         }
     }
 
-    Component {
-        id: attachmentPopover
+    Timer {
+        id: typingTimer
+        interval: 6000
+        onTriggered: {
+            messages.userTyping = false;
+        }
+    }
 
-        Popover {
-            id: popover
-            Column {
-                id: containerLayout
-                anchors {
-                    left: parent.left
-                    top: parent.top
-                    right: parent.right
+    Repeater {
+        model: messages.chatEntry ? messages.chatEntry.chatStates : null
+        Item {
+            function processChatState() {
+                if (modelData.state == ChatEntry.ChannelChatStateComposing) {
+                    messages.userTyping = true
+                    typingTimer.start()
+                } else {
+                    messages.userTyping = false
                 }
-                ListItem.Standard {
-                    text: i18n.tr("Remove")
-                    onClicked: {
-                        attachments.remove(activeAttachmentIndex)
-                        PopupUtils.close(popover)
+            }
+            Component.onCompleted: processChatState()
+            Connections {
+                target: modelData
+                onStateChanged: processChatState()
+            }
+        }
+    }
+
+    MessagesHeader {
+        id: headerContents
+        width: parent ? parent.width - units.gu(2) : undefined
+        height: units.gu(5)
+        title: pageHeader.title
+        subtitle: {
+            if (userTyping) {
+                return i18n.tr("Typing..")
+            }
+            switch (presenceRequest.type) {
+            case PresenceRequest.PresenceTypeAvailable:
+                return i18n.tr("Online")
+            case PresenceRequest.PresenceTypeOffline:
+                return i18n.tr("Offline")
+            case PresenceRequest.PresenceTypeAway:
+                return i18n.tr("Away")
+            case PresenceRequest.PresenceTypeBusy:
+                return i18n.tr("Busy")
+            default:
+                return ""
+            }
+        }
+        visible: true
+    }
+
+    PresenceRequest {
+        id: presenceItem
+        accountId: {
+            // if this is a regular sms chat, try requesting the presence on
+            // a multimedia account
+            if (!account) {
+                return ""
+            }
+            if (account.type == AccountEntry.PhoneAccount) {
+                for (var i in telepathyHelper.accounts) {
+                    var tmpAccount = telepathyHelper.accounts[i]
+                    if (tmpAccount.type == AccountEntry.MultimediaAccount) {
+                        return tmpAccount.accountId
                     }
                 }
+                return ""
             }
-            Component.onDestruction: activeAttachmentIndex = -1
-        }
+            return account.accountId
+        }
+        // we just request presence on 1-1 chats
+        identifier: participants.length == 1 ? participants[0].identifier : ""
+    }
+
+    ActivityIndicator {
+        id: activityIndicator
+        anchors {
+            verticalCenter: parent.verticalCenter
+            horizontalCenter: parent.horizontalCenter
+        }
+        running: isSearching
+        visible: running
     }
 
     Component {
@@ -727,13 +935,6 @@
         }
     }
 
-    Connections {
-        target: messages.head.sections
-        onSelectedIndexChanged: {
-            messages.account = messages.accountsModel[head.sections.selectedIndex]
-        }
-    }
-
     Loader {
         id: searchListLoader
 
@@ -745,10 +946,10 @@
         visible: source != ""
         anchors {
             top: parent.top
-            topMargin: units.gu(2)
+            topMargin: header.height + units.gu(2)
             left: parent.left
             right: parent.right
-            bottom: bottomPanel.top
+            bottom: composeBar.top
         }
         z: 1
         Behavior on height {
@@ -805,160 +1006,6 @@
         addressableFields: messages.account ? messages.account.addressableVCardFields : ["tel"] // just to have a fallback there
     }
 
-    onAccountsModelChanged: {
-        reloadFilters = !reloadFilters
-    }
-
-    Action {
-        id: backButton
-        objectName: "backButton"
-        iconName: "back"
-        onTriggered: {
-            if (typeof mainPage !== 'undefined') {
-                mainPage.temporaryProperties = null
-            }
-            mainStack.pop()
-        }
-    }
-
-    states: [
-        PageHeadState {
-            name: "selection"
-            head: messages.head
-            when: selectionMode
-
-            backAction: Action {
-                objectName: "selectionModeCancelAction"
-                iconName: "back"
-                onTriggered: messageList.cancelSelection()
-            }
-
-            actions: [
-                Action {
-                    objectName: "selectionModeSelectAllAction"
-                    iconName: "select"
-                    onTriggered: {
-                        if (messageList.selectedItems.count === messageList.count) {
-                            messageList.clearSelection()
-                        } else {
-                            messageList.selectAll()
-                        }
-                    }
-                },
-                Action {
-                    objectName: "selectionModeDeleteAction"
-                    enabled: messageList.selectedItems.count > 0
-                    iconName: "delete"
-                    onTriggered: messageList.endSelection()
-                }
-            ]
-        },
-        PageHeadState {
-            name: "groupChat"
-            head: messages.head
-            when: groupChat
-            contents: header
-            backAction: backButton
-
-            actions: [
-                Action {
-                    objectName: "groupChatAction"
-                    iconName: "contact-group"
-                    onTriggered: PopupUtils.open(participantsPopover, screenTop)
-                }
-            ]
-        },
-        PageHeadState {
-            name: "unknownContact"
-            head: messages.head
-            when: participants.length == 1 && contactWatcher.isUnknown
-            backAction: backButton
-            contents: header
-
-            actions: [
-                Action {
-                    objectName: "contactCallAction"
-                    visible: participants.length == 1 && contactWatcher.interactive
-                    iconName: "call-start"
-                    text: i18n.tr("Call")
-                    onTriggered: {
-                        Qt.inputMethod.hide()
-                        // FIXME: support other things than just phone numbers
-                        Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
-                    }
-                },
-                Action {
-                    objectName: "addContactAction"
-                    visible: contactWatcher.isUnknown && participants.length == 1 && contactWatcher.interactive
-                    iconName: "contact-new"
-                    text: i18n.tr("Add")
-                    onTriggered: {
-                        Qt.inputMethod.hide()
-                        // FIXME: support other things than just phone numbers
-                        mainView.addPhoneToContact("", contactWatcher.identifier, null, null)
-                    }
-                }
-            ]
-        },
-        PageHeadState {
-            name: "newMessage"
-            head: messages.head
-            when: participants.length === 0 && isReady
-            backAction: backButton
-            actions: [
-                Action {
-                    objectName: "contactList"
-                    iconName: "contact"
-                    onTriggered: {
-                        Qt.inputMethod.hide()
-                        mainStack.push(Qt.resolvedUrl("NewRecipientPage.qml"), {"multiRecipient": multiRecipient, "parentPage": messages})
-                    }
-                }
-
-            ]
-
-            contents: MultiRecipientInput {
-                id: multiRecipient
-                objectName: "multiRecipient"
-                enabled: visible
-                anchors {
-                    left: parent ? parent.left : undefined
-                    right: parent ? parent.right : undefined
-                    rightMargin: units.gu(2)
-                }
-            }
-        },
-        PageHeadState {
-            name: "knownContact"
-            head: messages.head
-            when: participants.length == 1 && !contactWatcher.isUnknown
-            backAction: backButton
-            contents: header
-            actions: [
-                Action {
-                    objectName: "contactCallKnownAction"
-                    visible: participants.length == 1 && messages.phoneAccount
-                    iconName: "call-start"
-                    text: i18n.tr("Call")
-                    onTriggered: {
-                        Qt.inputMethod.hide()
-                        // FIXME: support other things than just phone numbers
-                        Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
-                    }
-                },
-                Action {
-                    objectName: "contactProfileAction"
-                    visible: !contactWatcher.isUnknown && participants.length == 1 && messages.phoneAccount
-                    iconSource: "image://theme/contact"
-                    text: i18n.tr("Contact")
-                    onTriggered: {
-                        mainView.showContactDetails(contactWatcher.contactId, null, null)
-                    }
-                }
-            ]
-        }
-    ]
-
     HistoryEventModel {
         id: eventModel
         type: HistoryThreadModel.EventTypeText
@@ -1011,7 +1058,7 @@
     Item {
         id: screenTop
         anchors {
-            top: parent.top
+            top: pageHeader.bottom
             left: parent.left
             right: parent.right
         }
@@ -1023,392 +1070,125 @@
         objectName: "messageList"
         visible: !isSearching
 
+        Rectangle {
+            color: Theme.palette.normal.background
+            anchors.fill: parent
+            Image {
+                width: units.gu(20)
+                fillMode: Image.PreserveAspectFit
+                anchors.centerIn: parent
+                visible: source !== ""
+                source: {
+                    var accountId = ""
+
+                    if (messages.account) {
+                        accountId = messages.account.accountId
+                    }
+
+                    if (presenceRequest.type != PresenceRequest.PresenceTypeUnknown
+                            && presenceRequest.type != PresenceRequest.PresenceTypeUnset) {
+                        accountId = presenceRequest.accountId
+                    }
+
+                    return telepathyHelper.accountForId(accountId).protocolInfo.backgroundImage
+                }
+                z: 1
+            }
+            z: -1
+        }
+
         // because of the header
         clip: true
         anchors {
             top: screenTop.bottom
             left: parent.left
             right: parent.right
-            bottom: bottomPanel.top
+            bottom: composeBar.top
         }
     }
 
-    Item {
-        id: bottomPanel
-        property int defaultHeight: textEntry.height + units.gu(2)
-        anchors.bottom: isSearching ? parent.bottom : keyboard.top
-        anchors.left: parent.left
-        anchors.right: parent.right
-        height: {
-            if (selectionMode || (participants.length > 0 && !contactWatcher.interactive)) {
-                return 0
-            } else {
-                if (messages.height - keyboard.height - screenTop.y > defaultHeight) {
-                    return defaultHeight
-                } else {
-                    return messages.height - keyboard.height - screenTop.y
-                }
-            }
-        }
-        visible: !selectionMode && !isSearching
-        clip: true
-        MouseArea {
-            anchors.fill: parent
-            onClicked: {
-                messageTextArea.forceActiveFocus()
-            }
-        }
-
-        Behavior on height {
-            UbuntuNumberAnimation { }
-        }
-
-        ListItem.ThinDivider {
-            anchors.top: parent.top
-        }
-
-        Icon {
-            id: attachButton
-            objectName: "attachButton"
-            anchors.left: parent.left
-            anchors.leftMargin: units.gu(2)
-            anchors.verticalCenter: sendButton.verticalCenter
-            height: units.gu(3)
-            width: units.gu(3)
-            color: "gray"
-            name: "camera-app-symbolic"
-            MouseArea {
-                anchors.fill: parent
-                anchors.margins: units.gu(-2)
-                onClicked: {
-                    Qt.inputMethod.hide()
-                    pictureImporter.requestNewPicture()
-                }
-            }
-        }
-
-        StyledItem {
-            id: textEntry
-            property alias text: messageTextArea.text
-            property alias inputMethodComposing: messageTextArea.inputMethodComposing
-            property int fullSize: attachmentThumbnails.height + messageTextArea.height
-            style: Theme.createStyleComponent("TextAreaStyle.qml", textEntry)
-            anchors.bottomMargin: units.gu(1)
-            anchors.bottom: parent.bottom
-            anchors.left: attachButton.right
-            anchors.leftMargin: units.gu(1)
-            anchors.right: sendButton.left
-            anchors.rightMargin: units.gu(1)
-            height: attachments.count !== 0 ? fullSize + units.gu(1.5) : fullSize
-            onActiveFocusChanged: {
-                if(activeFocus) {
-                    messageTextArea.forceActiveFocus()
-                } else {
-                    focus = false
-                }
-            }
-            focus: false
-            MouseArea {
-                anchors.fill: parent
-                onClicked: messageTextArea.forceActiveFocus()
-            }
-            Flow {
-                id: attachmentThumbnails
-                spacing: units.gu(1)
-                anchors{
-                    left: parent.left
-                    right: parent.right
-                    top: parent.top
-                    topMargin: units.gu(1)
-                    leftMargin: units.gu(1)
-                    rightMargin: units.gu(1)
-                }
-                height: childrenRect.height
-
-                Component {
-                    id: thumbnailImage
-                    UbuntuShape {
-                        property int index
-                        property string filePath
-
-                        width: childrenRect.width
-                        height: childrenRect.height
-
-                        image: Image {
-                            id: avatarImage
-                            width: units.gu(8)
-                            height: units.gu(8)
-                            sourceSize.height: height
-                            sourceSize.width: width
-                            fillMode: Image.PreserveAspectCrop
-                            source: filePath
-                            asynchronous: true
-                        }
-                        MouseArea {
-                            anchors.fill: parent
-                            onPressAndHold: {
-                                mouse.accept = true
-                                Qt.inputMethod.hide()
-                                activeAttachmentIndex = index
-                                PopupUtils.open(attachmentPopover, parent)
-                            }
-                        }
-                    }
-                }
-
-                Component {
-                    id: thumbnailContact
-                    Item {
-                        id: attachment
-
-                        readonly property int contactsCount:vcardParser.contacts ? vcardParser.contacts.length : 0
-                        property int index
-                        property string filePath
-                        property alias vcard: vcardParser
-                        property string contactDisplayName: {
-                            if (contactsCount > 0)  {
-                                var contact = vcard.contacts[0]
-                                if (contact.displayLabel.label && (contact.displayLabel.label != "")) {
-                                    return contact.displayLabel.label
-                                } else if (contact.name) {
-                                    var contacFullName  = contact.name.firstName
-                                    if (contact.name.midleName) {
-                                        contacFullName += " " + contact.name.midleName
-                                    }
-                                    if (contact.name.lastName) {
-                                        contacFullName += " " + contact.name.lastName
-                                    }
-                                    return contacFullName
-                                }
-                                return i18n.tr("Unknown contact")
-                            }
-                            return ""
-                        }
-                        property string title: {
-                            var result = attachment.contactDisplayName
-                            if (attachment.contactsCount > 1) {
-                                return result + " (+%1)".arg(attachment.contactsCount-1)
-                            } else {
-                                return result
-                            }
-                        }
-
-                        height: units.gu(6)
-                        width: textEntry.width
-
-                        ContactAvatar {
-                            id: avatar
-
-                            anchors {
-                                top: parent.top
-                                bottom: parent.bottom
-                                left: parent.left
-                            }
-                            contactElement: attachment.contactsCount === 1 ? attachment.vcard.contacts[0] : null
-                            fallbackAvatarUrl: attachment.contactsCount === 1 ? "image://theme/contact" : "image://theme/contact-group"
-                            fallbackDisplayName: attachment.contactsCount === 1 ? attachment.contactDisplayName : ""
-                            width: height
-                        }
-                        Label {
-                            id: label
-
-                            anchors {
-                                left: avatar.right
-                                leftMargin: units.gu(1)
-                                top: avatar.top
-                                bottom: avatar.bottom
-                                right: parent.right
-                                rightMargin: units.gu(1)
-                            }
-
-                            verticalAlignment: Text.AlignVCenter
-                            text: attachment.title
-                            elide: Text.ElideMiddle
-                            color: UbuntuColors.lightAubergine
-                        }
-                        MouseArea {
-                            anchors.fill: parent
-                            onPressAndHold: {
-                                mouse.accept = true
-                                Qt.inputMethod.hide()
-                                activeAttachmentIndex = index
-                                PopupUtils.open(attachmentPopover, parent)
-                            }
-                        }
-                        VCardParser {
-                            id: vcardParser
-
-                            vCardUrl: attachment ? Qt.resolvedUrl(attachment.filePath) : ""
-                        }
-                    }
-                }
-
-                Component {
-                    id: thumbnailUnknown
-
-                    UbuntuShape {
-                        property int index
-                        property string filePath
-
-                        width: units.gu(8)
-                        height: units.gu(8)
-
-                        Icon {
-                            anchors.centerIn: parent
-                            width: units.gu(6)
-                            height: units.gu(6)
-                            name: "attachment"
-                        }
-                        MouseArea {
-                            anchors.fill: parent
-                            onPressAndHold: {
-                                mouse.accept = true
-                                Qt.inputMethod.hide()
-                                activeAttachmentIndex = index
-                                PopupUtils.open(attachmentPopover, parent)
-                            }
-                        }
-                    }
-                }
-
-                Repeater {
-                    model: attachments
-                    delegate: Loader {
-                        height: units.gu(8)
-                        sourceComponent: {
-                            var contentType = getContentType(filePath)
-                            console.log(contentType)
-                            switch(contentType) {
-                            case ContentType.Contacts:
-                                return thumbnailContact
-                            case ContentType.Pictures:
-                                return thumbnailImage
-                            case ContentType.Unknown:
-                                return thumbnailUnknown
-                            default:
-                                console.log("unknown content Type")
-                            }
-                        }
-                        onStatusChanged: {
-                            if (status == Loader.Ready) {
-                                item.index = index
-                                item.filePath = filePath
-                            }
-                        }
-                    }
-                }
-            }
-
-            ListItem.ThinDivider {
-                id: divider
-
-                anchors {
-                    left: parent.left
-                    right: parent.right
-                    top: attachmentThumbnails.bottom
-                    margins: units.gu(0.5)
-                }
-                visible: attachments.count > 0
-            }
-
-            TextArea {
-                id: messageTextArea
-                objectName: "messageTextArea"
-                anchors {
-                    top: attachments.count == 0 ? textEntry.top : attachmentThumbnails.bottom
-                    left: parent.left
-                    right: parent.right
-                }
-                // this value is to avoid letter being cut off
-                height: units.gu(4.3)
-                style: LocalTextAreaStyle {}
-                autoSize: true
-                maximumLineCount: attachments.count == 0 ? 8 : 4
-                placeholderText: i18n.tr("Write a message...")
-                focus: textEntry.focus
-                font.family: "Ubuntu"
-                font.pixelSize: FontUtils.sizeToPixels("medium")
-                color: "#5d5d5d"
-                text: messages.text
-            }
-
-            /*InverseMouseArea {
-                anchors.fill: parent
-                visible: textEntry.activeFocus
-                onClicked: {
-                    textEntry.focus = false;
-                }
-            }*/
-            Component.onCompleted: {
-                // if page is active, it means this is not a bottom edge page
-                if (messages.active && messages.keyboardFocus && participants.length != 0) {
-                    messageTextArea.forceActiveFocus()
-                }
-            }
-        }
-
-        Icon {
-            id: sendButton
-            objectName: "sendButton"
-            anchors.verticalCenter: textEntry.verticalCenter
-            anchors.right: parent.right
-            anchors.rightMargin: units.gu(2)
-            color: "gray"
-            source: Qt.resolvedUrl("./assets/send.svg")
-            width: units.gu(3)
-            height: units.gu(3)
-            enabled: {
-               if (participants.length > 0 || multiRecipient.recipientCount > 0 || multiRecipient.searchString !== "") {
-                    if (textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0) {
-                        return true
-                    }
-                }
-                return false
-            }
-
-            MouseArea {
-                anchors.fill: parent
-                anchors.margins: units.gu(-2)
-                onClicked: {
-                    // make sure we flush everything we have prepared in the OSK preedit
-                    Qt.inputMethod.commit();
-                    if (textEntry.text == "" && attachments.count == 0) {
-                        return
-                    }
-                    // refresh the recipient list
-                    multiRecipient.focus = false
-
-                    if (messages.account && messages.accountId == "") {
-                        messages.accountId = messages.account.accountId
-                        messages.head.sections.selectedIndex = Qt.binding(getSelectedIndex)
-                    }
-
-                    var newAttachments = []
-                    for (var i = 0; i < attachments.count; i++) {
-                        var attachment = []
-                        var item = attachments.get(i)
-                        // we dont include smil files. they will be auto generated
-                        if (item.contentType.toLowerCase() === "application/smil") {
-                            continue
-                        }
-                        attachment.push(item.name)
-                        attachment.push(item.contentType)
-                        attachment.push(item.filePath)
-                        newAttachments.push(attachment)
-                    }
-
-                    var recipients = participantIds.length > 0 ? participantIds :
-                                                                 multiRecipient.recipients
-                    // if sendMessage succeeds it means the message was either sent or
-                    // injected into the history service so the user can retry later
-                    if (sendMessage(textEntry.text, recipients, newAttachments)) {
-                        textEntry.text = ""
-                        attachments.clear()
-                    }
-                    if (eventModel.filter == null) {
-                        reloadFilters = !reloadFilters
-                    }
-                }
+    ComposeBar {
+        id: composeBar
+        anchors {
+            bottom: isSearching ? parent.bottom : keyboard.top
+            left: parent.left
+            right: parent.right
+        }
+
+        showContents: !selectionMode && !isSearching
+        maxHeight: messages.height - keyboard.height - screenTop.y
+        text: messages.text
+        canSend: participants.length > 0 || multiRecipient.recipientCount > 0 || multiRecipient.searchString !== ""
+        oskEnabled: messages.oskEnabled
+
+        Component.onCompleted: {
+            // if page is active, it means this is not a bottom edge page
+            if (messages.active && messages.keyboardFocus && participants.length != 0) {
+                forceFocus()
+            }
+        }
+
+        onSendRequested: {
+            // refresh the recipient list
+            multiRecipient.focus = false
+
+            if (messages.account && messages.accountId == "") {
+                messages.accountId = messages.account.accountId
+                messages.head.sections.selectedIndex = Qt.binding(getSelectedIndex)
+            }
+
+            var newAttachments = []
+            var videoSize = 0;
+            for (var i = 0; i < attachments.count; i++) {
+                var attachment = []
+                var item = attachments.get(i)
+                // we dont include smil files. they will be auto generated
+                if (item.contentType.toLowerCase() === "application/smil") {
+                    continue
+                }
+                if (startsWith(item.contentType.toLowerCase(),"video/")) {
+                    videoSize += FileOperations.size(item.filePath)
+                }
+                attachment.push(item.name)
+                attachment.push(item.contentType)
+                attachment.push(item.filePath)
+                newAttachments.push(attachment)
+            }
+            if (videoSize > 307200 && !settings.messagesDontShowFileSizeWarning) {
+                // FIXME we are guessing here if the handler will try to send it over multimedia account
+                var isPhone = (account && account.type == AccountEntry.PhoneAccount)
+                if (isPhone) {
+                    for (var i in telepathyHelper.accounts) {
+                        var tmpAccount = telepathyHelper.accounts[i]
+                        if (tmpAccount.type == AccountEntry.MultimediaAccount) {
+                            // now check if the user is at least known by the account
+                            if (presenceRequest.type != PresenceRequest.PresenceTypeUnknown
+                                     && presenceRequest.type != PresenceRequest.PresenceTypeUnset) {
+                                isPhone = false
+                            }
+                        }
+                    }
+                }
+ 
+                if (isPhone) {
+                    PopupUtils.open(Qt.createComponent("Dialogs/FileSizeWarningDialog.qml").createObject(messages))
+                }
+            }
+
+            var recipients = participantIds.length > 0 ? participantIds :
+                                                         multiRecipient.recipients
+            var properties = {}
+            if (composeBar.audioAttached) {
+                properties["x-canonical-tmp-files"] = true
+            }
+
+            // if sendMessage succeeds it means the message was either sent or
+            // injected into the history service so the user can retry later
+            if (sendMessage(text, recipients, newAttachments, properties)) {
+                composeBar.reset()
+            }
+            if (eventModel.filter == null) {
+                reloadFilters = !reloadFilters
             }
         }
     }
@@ -1435,4 +1215,6 @@
             }
         }
     }
+
+    // FIXME: we need a bottom edge here somehow
 }

=== modified file 'src/qml/MessagesHeader.qml'
--- src/qml/MessagesHeader.qml	2015-08-04 01:06:06 +0000
+++ src/qml/MessagesHeader.qml	2016-02-05 00:58:50 +0000
@@ -18,7 +18,7 @@
 
 
 import QtQuick 2.2
-import Ubuntu.Components 1.1
+import Ubuntu.Components 1.3
 
 Item {
     id: header
@@ -26,7 +26,12 @@
     property string title: ""
     property string subtitle: ""
 
-    height: units.gu(7)
+    height: units.gu(8)
+
+    anchors {
+        top: parent.top
+        topMargin: units.gu(1)
+    }
 
     Behavior on height {
         UbuntuNumberAnimation {}
@@ -50,7 +55,7 @@
             }
             verticalAlignment: Text.AlignVCenter
 
-            font.pixelSize: FontUtils.sizeToPixels("x-large")
+            font.pixelSize: FontUtils.sizeToPixels("large")
             elide: Text.ElideRight
             text: title
         }

=== added file 'src/qml/MessagingBottomEdge.qml'
--- src/qml/MessagingBottomEdge.qml	1970-01-01 00:00:00 +0000
+++ src/qml/MessagingBottomEdge.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,43 @@
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+BottomEdge {
+    id: bottomEdge
+
+    function commitWithProperties(properties) {
+        _realPage = messagesComponent.createObject(null, properties)
+        commit()
+    }
+
+    property var _realPage: null
+
+    height: parent ? parent.height : 0
+    hint.text: i18n.tr("+")
+    contentComponent: Item {
+        id: pageContent
+        implicitWidth: bottomEdge.width
+        implicitHeight: bottomEdge.height
+        children: bottomEdge._realPage
+    }
+
+    Component.onCompleted: {
+        mainView.bottomEdge = bottomEdge
+        _realPage = messagesComponent.createObject(null)
+    }
+
+    onCollapseCompleted: {
+        _realPage = messagesComponent.createObject(null)
+    }
+
+    Component {
+        id: messagesComponent
+
+        Messages {
+            anchors.fill: parent
+            onCancel: bottomEdge.collapse()
+            basePage: bottomEdge.parent
+            startedFromBottomEdge: true
+        }
+    }
+
+}

=== modified file 'src/qml/MessagingContactEditorPage.qml'
--- src/qml/MessagingContactEditorPage.qml	2016-01-06 18:29:47 +0000
+++ src/qml/MessagingContactEditorPage.qml	2016-02-05 00:58:50 +0000
@@ -52,8 +52,12 @@
 
     onContactSaved: {
         if (root.contactListPage) {
-            root.contactListPage.moveListToContact(contact)
-            root.contactListPage.phoneToAdd = ""
+            if (root.contactListPage.phoneToAdd !== "") {
+                mainStack.removePage(root.contactListPage)
+            } else {
+                root.contactListPage.moveListToContact(contact)
+                root.contactListPage.phoneToAdd = ""
+            }
         }
     }
 }

=== modified file 'src/qml/MessagingContactViewPage.qml'
--- src/qml/MessagingContactViewPage.qml	2016-01-06 18:29:47 +0000
+++ src/qml/MessagingContactViewPage.qml	2016-02-05 00:58:50 +0000
@@ -42,12 +42,13 @@
         var newDetail = Qt.createQmlObject(detailSourceTemplate, contact)
         if (newDetail) {
             contact.addDetail(newDetail)
-            pageStack.push(root.contactEditorPageURL,
-                           { model: root.model,
-                             contact: contact,
-                             initialFocusSection: "phones",
-                             newDetails: [newDetail],
-                             contactListPage: root.contactListPage })
+            mainStack.addPageToCurrentColumn(root,
+                                             root.contactEditorPageURL,
+                                             { model: root.model,
+                                               contact: contact,
+                                               initialFocusSection: "phones",
+                                               newDetails: [newDetail],
+                                               contactListPage: root.contactListPage })
             root.addPhoneToContact = ""
         } else {
             console.warn("Fail to create phone number detail")
@@ -61,7 +62,7 @@
             iconName: "share"
             visible: root.editable
             onTriggered: {
-                pageStack.push(contactShareComponent,
+                pageStack.addComponentToCurrentColumnSync(root, contactShareComponent,
                                { contactModel: root.model,
                                  contacts: [root.contact] })
             }
@@ -72,7 +73,7 @@
             iconName: "edit"
             visible: root.editable
             onTriggered: {
-                pageStack.push(contactEditorPageURL,
+                pageStack.addFileToCurrentColumnSync(root, contactEditorPageURL,
                                { model: root.model,
                                  contact: root.contact,
                                  contactListPage: root.contactListPage })
@@ -122,9 +123,9 @@
         } else {
             Qt.openUrlExternally(("%1:%2").arg(action).arg(detail.value(0)))
         }
-        pageStack.pop()
+        pageStack.removePage(root)
     }
-    onContactRemoved: pageStack.pop()
+    onContactRemoved: pageStack.removePage(root)
     onContactFetched: {
         root.contact = contact
         if (root.active && root.addPhoneToContact != "") {

=== added file 'src/qml/MessagingPageLayout.qml'
--- src/qml/MessagingPageLayout.qml	1970-01-01 00:00:00 +0000
+++ src/qml/MessagingPageLayout.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,64 @@
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+AdaptivePageLayout {
+    id: layout
+    property var _pagesToRemove: []
+
+    function deleteInstances() {
+        for (var i in _pagesToRemove) {
+            if (_pagesToRemove[i].destroy) {
+                _pagesToRemove[i].destroy()
+            }
+        }
+        _pagesToRemove = []
+    }
+
+    function removePage(page) {
+        // check if this page was allocated dynamically and then remove it
+        for (var i in _pagesToRemove) {
+            if (_pagesToRemove[i] == page) {
+                _pagesToRemove[i].destroy()
+                _pagesToRemove.splice(i, 1)
+                break
+            }
+        }
+        removePages(page)
+    }
+
+    function addFileToNextColumnSync(parentObject, resolvedUrl, properties) {
+        if (typeof(properties) === 'undefined') {
+            properties = {}
+        }
+        var page = Qt.createComponent(resolvedUrl).createObject(parentObject, properties)
+        layout.addPageToNextColumn(parentObject, page)
+        _pagesToRemove.push(page)
+    }
+
+    function addFileToCurrentColumnSync(parentObject, resolvedUrl, properties) {
+        if (typeof(properties) === 'undefined') {
+            properties = {}
+        }
+        var page = Qt.createComponent(resolvedUrl).createObject(parentObject, properties)
+        layout.addPageToCurrentColumn(parentObject, page)
+        _pagesToRemove.push(page)
+    }
+
+    function addComponentToNextColumnSync(parentObject, component, properties) {
+        if (typeof(properties) === 'undefined') {
+            properties = {}
+        }
+        var page = component.createObject(parentObject, properties)
+        layout.addPageToNextColumn(parentObject, page)
+        _pagesToRemove.push(page)
+    }
+
+    function addComponentToCurrentColumnSync(parentObject, component, properties) {
+        if (typeof(properties) === 'undefined') {
+            properties = {}
+        }
+        var page = component.createObject(parentObject, properties)
+        layout.addPageToCurrentColumn(parentObject, page)
+        _pagesToRemove.push(page)
+    }
+}

=== modified file 'src/qml/MultiRecipientInput.qml'
--- src/qml/MultiRecipientInput.qml	2015-09-14 13:51:27 +0000
+++ src/qml/MultiRecipientInput.qml	2016-02-05 00:58:50 +0000
@@ -29,7 +29,7 @@
     property variant recipients: []
     property string searchString: ""
     signal clearSearch()
-    style: Theme.createStyleComponent("TextFieldStyle.qml", multiRecipientWidget)
+    styleName: "TextFieldStyle"
     clip: true
     height: contactFlow.height
     focus: activeFocus

=== modified file 'src/qml/NewRecipientPage.qml'
--- src/qml/NewRecipientPage.qml	2015-11-16 22:16:06 +0000
+++ src/qml/NewRecipientPage.qml	2016-02-05 00:58:50 +0000
@@ -26,7 +26,6 @@
     objectName: "newRecipientPage"
 
     property Item multiRecipient: null
-    property Item parentPage: null
     property string phoneToAdd: ""
     property QtObject contactIndex: null
 
@@ -44,11 +43,39 @@
     {
         multiRecipient.addRecipient(phoneNumber)
         multiRecipient.forceActiveFocus()
-        mainStack.pop()
-    }
-
-    title: i18n.tr("Add recipient")
-
+        mainStack.removePage(newRecipientPage)
+    }
+
+    header: PageHeader {
+        id: pageHeader
+
+        property alias leadingActions: leadingBar.actions
+        property alias trailingActions: trailingBar.actions
+
+        title: i18n.tr("Add recipient")
+        leadingActionBar {
+            id: leadingBar
+        }
+
+        trailingActionBar {
+            id: trailingBar
+            actions: [
+                Action {
+                    text: i18n.tr("Back")
+                    iconName: "back"
+                    onTriggered: {
+                        mainStack.removePage(newRecipientPage)
+                        newRecipientPage.destroy()
+                    }
+                }
+            ]
+        }
+    }
+
+    Sections {
+        id: headerSections
+        model: [i18n.tr("All"), i18n.tr("Favorites")]
+    }
     TextField {
         id: searchField
 
@@ -69,11 +96,10 @@
 
     state: "default"
     states: [
-        PageHeadState {
+        State {
             id: defaultState
-
             name: "default"
-            actions: [
+            property list<QtObject> trailingActions: [
                 Action {
                     text: i18n.tr("Search")
                     iconName: "search"
@@ -84,10 +110,11 @@
                     }
                 }
             ]
+
             PropertyChanges {
-                target: newRecipientPage.head
-                actions: defaultState.actions
-                sections.model: [i18n.tr("All"), i18n.tr("Favorites")]
+                target: pageHeader
+                trailingActions: defaultState.trailingActions
+                extension: headerSections
             }
             PropertyChanges {
                 target: searchField
@@ -95,27 +122,34 @@
                 visible: false
             }
         },
-        PageHeadState {
+        State {
             id: searchingState
-
             name: "searching"
-            backAction: Action {
-                iconName: "back"
-                text: i18n.tr("Cancel")
-                onTriggered: {
-                    newRecipientPage.forceActiveFocus()
-                    newRecipientPage.state = "default"
-                    newRecipientPage.head.sections.selectedIndex = 0
+            property list<QtObject> leadingActions: [
+                Action {
+                    iconName: "back"
+                    text: i18n.tr("Cancel")
+                    onTriggered: {
+                        newRecipientPage.forceActiveFocus()
+                        newRecipientPage.state = "default"
+                        headerSections.selectedIndex = 0
+                    }
                 }
-            }
+            ]
 
             PropertyChanges {
-                target: newRecipientPage.head
-                backAction: searchingState.backAction
+                target: pageHeader
+                leadingActions: searchingState.leadingActions
+                trailingActions: []
                 contents: searchField
             }
 
             PropertyChanges {
+                target: headerSections
+                visible: false
+            }
+
+            PropertyChanges {
                 target: searchField
                 text: ""
                 visible: true
@@ -127,7 +161,7 @@
         id: contactList
         objectName: "newRecipientList"
         anchors {
-            top: parent.top
+            top: pageHeader.bottom
             left: parent.left
             right: parent.right
             bottom: keyboard.top
@@ -139,12 +173,14 @@
         filterTerm: searchField.text
         onContactClicked: {
             if (newRecipientPage.phoneToAdd != "") {
-                mainView.addPhoneToContact(contact,
+                mainView.addPhoneToContact(newRecipientPage,
+                                           contact,
                                            newRecipientPage.phoneToAdd,
                                            newRecipientPage,
                                            contactList.listModel)
             } else {
-                mainView.showContactDetails(contact,
+                mainView.showContactDetails(newRecipientPage,
+                                            contact,
                                             newRecipientPage,
                                             contactList.listModel)
             }
@@ -152,24 +188,20 @@
 
         onAddNewContactClicked: {
             var newContact = ContactsJS.createEmptyContact(newRecipientPage.phoneToAdd, newRecipientPage)
-            pageStack.push(Qt.resolvedUrl("MessagingContactEditorPage.qml"),
-                           { model: contactList.listModel,
-                             contact: newContact,
-                             initialFocusSection: (newRecipientPage.phoneToAdd != "" ? "phones" : "name"),
-                             contactListPage: newRecipientPage
-                           })
+            mainStack.addFileToCurrentColumnSync(newRecipientPage,
+                                             Qt.resolvedUrl("MessagingContactEditorPage.qml"),
+                                             { model: contactList.listModel,
+                                               contact: newContact,
+                                               initialFocusSection: (newRecipientPage.phoneToAdd != "" ? "phones" : "name"),
+                                               contactListPage: newRecipientPage })
         }
     }
 
-    // WORKAROUND: This is necessary to make the header visible from a bottom edge page
     Component.onCompleted: {
-        parentPage.active = false
         if (QTCONTACTS_PRELOAD_VCARD !== "") {
             contactList.listModel.importContacts("file://" + QTCONTACTS_PRELOAD_VCARD)
         }
     }
-    Component.onDestruction: parentPage.active = true
-
     onActiveChanged: {
         if (active && (state === "searching")) {
             searchField.forceActiveFocus()

=== modified file 'src/qml/RegularMessageDelegate.qml'
--- src/qml/RegularMessageDelegate.qml	2015-08-05 19:31:15 +0000
+++ src/qml/RegularMessageDelegate.qml	2016-02-05 00:58:50 +0000
@@ -57,7 +57,7 @@
         selectionMode: root.isInSelectionMode
         accountLabel: {
             var account = telepathyHelper.accountForId(accountId)
-            if (account.type == AccountEntry.PhoneAccount || account.type == AccountEntry.MultimediaAccount) {
+            if (account && (account.type == AccountEntry.PhoneAccount || account.type == AccountEntry.MultimediaAccount)) {
                 if (multiplePhoneAccounts) {
                     return account.displayName
                 }

=== modified file 'src/qml/SMSDelegate.qml'
--- src/qml/SMSDelegate.qml	2015-10-23 20:41:36 +0000
+++ src/qml/SMSDelegate.qml	2016-02-05 00:58:50 +0000
@@ -73,5 +73,6 @@
         messageTimeStamp: root.messageData.timestamp
         accountName: root.accountLabel
         messageStatus: root.messageData.textMessageStatus
+        showDeliveryStatus: true
     }
 }

=== modified file 'src/qml/SettingsPage.qml'
--- src/qml/SettingsPage.qml	2015-09-14 13:51:27 +0000
+++ src/qml/SettingsPage.qml	2016-02-05 00:58:50 +0000
@@ -35,6 +35,11 @@
         schema.id: "com.ubuntu.phone"
     }
 
+    header: PageHeader {
+        id: pageHeader
+        title: settingsPage.title
+    }
+
     Component {
         id: settingDelegate
         Item {
@@ -66,9 +71,25 @@
     }
 
     ListView {
-        anchors.fill: parent
+        anchors {
+            top: pageHeader.bottom
+            left: parent.left
+            right: parent.right
+            bottom: parent.bottom
+        }
         model: settingsModel
         delegate: settingDelegate
     }
+
+    Loader {
+        id: messagesBottomEdgeLoader
+        active: mainView.dualPanel
+        sourceComponent: MessagingBottomEdge {
+            id: messagesBottomEdge
+            parent: settingsPage
+            hint.text: ""
+            hint.height: 0
+        }
+    }
 }
 

=== added directory 'src/qml/Stickers'
=== added file 'src/qml/Stickers/CMakeLists.txt'
--- src/qml/Stickers/CMakeLists.txt	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/CMakeLists.txt	2016-02-05 00:58:50 +0000
@@ -0,0 +1,4 @@
+file(GLOB STICKERS_QML_FILES *.qml)
+
+add_custom_target(messaging_app_Stickers_QMlFiles ALL SOURCES ${STICKERS_QML_FILES})
+install(FILES ${STICKERS_QML_FILES} DESTINATION ${MESSAGING_APP_DIR}/Stickers)

=== added file 'src/qml/Stickers/HistoryButton.qml'
--- src/qml/Stickers/HistoryButton.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/HistoryButton.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.3
+
+AbstractButton {
+    property bool selected
+
+    Rectangle {
+        anchors.fill: parent
+        color: selected ? "#f5f5f5" : "transparent"
+    }
+
+    Icon {
+        name: "history"
+        anchors.fill: parent
+        anchors.margins: units.gu(1.5)
+    }
+}

=== added file 'src/qml/Stickers/StickerDelegate.qml'
--- src/qml/Stickers/StickerDelegate.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/StickerDelegate.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.3
+
+AbstractButton {
+    property alias stickerSource: image.source
+
+    Image {
+        id: image
+        anchors.fill: parent
+        anchors.margins: units.gu(0.5)
+        fillMode: Image.PreserveAspectFit
+        smooth: true
+    }
+}

=== added file 'src/qml/Stickers/StickerPackDelegate.qml'
--- src/qml/Stickers/StickerPackDelegate.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/StickerPackDelegate.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Qt.labs.folderlistmodel 2.1
+import Ubuntu.Components 1.3
+
+AbstractButton {
+    property alias path: stickers.folder
+    property string name
+    property bool selected
+
+    Rectangle {
+        anchors.fill: parent
+        color: selected ? "#f5f5f5" : "transparent"
+    }
+
+    Icon {
+        anchors.fill: parent
+        visible: stickers.count === 0
+        name: "cancel"
+    }
+
+    Image {
+        visible: stickers.count > 0
+        anchors.fill: parent
+        anchors.margins: units.gu(0.5)
+        fillMode: Image.PreserveAspectFit
+        smooth: true
+        source: visible ? stickers.get(0, "filePath") : ""
+    }
+
+    FolderListModel {
+        id: stickers
+        showDirs: false
+        nameFilters: ["*.png", "*.webm", "*.gif"]
+    }
+}

=== added file 'src/qml/Stickers/StickerPacksModel.qml'
--- src/qml/Stickers/StickerPacksModel.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/StickerPacksModel.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Qt.labs.folderlistmodel 2.1
+
+FolderListModel {
+    folder: dataLocation + "/stickers/"
+    showFiles: false
+}

=== added file 'src/qml/Stickers/StickersModel.qml'
--- src/qml/Stickers/StickersModel.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/StickersModel.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Qt.labs.folderlistmodel 2.1
+
+FolderListModel {
+    property string packName
+    folder: "%1/stickers/%2".arg(dataLocation).arg(packName)
+    showDirs: false
+    nameFilters: ["*.png", "*.webm", "*.gif"]
+}

=== added file 'src/qml/Stickers/StickersPicker.qml'
--- src/qml/Stickers/StickersPicker.qml	1970-01-01 00:00:00 +0000
+++ src/qml/Stickers/StickersPicker.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.3
+import messagingapp.private 0.1
+
+FocusScope {
+    id: pickerRoot
+    signal stickerSelected(string path)
+
+    Component.onCompleted: {
+        StickersHistoryModel.databasePath = dataLocation + "/stickers/stickers.sqlite"
+        StickersHistoryModel.limit = 10
+    }
+
+    property bool expanded: false
+    readonly property int packCount: stickerPacksModel.count
+
+    // FIXME: try to get something similar to the keyboard height
+    // FIXME: animate the displaying
+    height: expanded ? units.gu(30) : 0
+    opacity: expanded ? 1 : 0
+    visible: opacity > 0
+
+    Connections {
+        target: Qt.inputMethod
+        onVisibleChanged: {
+            if (Qt.inputMethod.visible && oskEnabled) {
+                pickerRoot.expanded = false
+            }
+        }
+    }
+
+    Behavior on height {
+        UbuntuNumberAnimation { }
+    }
+
+    Behavior on opacity {
+        UbuntuNumberAnimation { }
+    }
+
+    ListView {
+        id: setsList
+        model: stickerPacksModel
+        orientation: ListView.Horizontal
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: parent.top
+        height: units.gu(6)
+
+        header: HistoryButton {
+            height: units.gu(6)
+            width: height
+
+            onTriggered: stickersGrid.model.packName = ""
+            selected: stickersGrid.model.packName === ""
+        }
+        delegate: StickerPackDelegate {
+            anchors.top: parent.top
+            anchors.bottom: parent.bottom
+            width: units.gu(6)
+
+            path: filePath
+            onTriggered: stickersGrid.model.packName = fileName
+            selected: stickersGrid.model.packName === fileName
+        }
+    }
+
+    Rectangle {
+        anchors.fill: stickersGrid
+        color: "#f5f5f5"
+    }
+
+    GridView {
+        id: stickersGrid
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: setsList.bottom
+        anchors.bottom: parent.bottom
+        clip: true
+        cellWidth: units.gu(10)
+        cellHeight: units.gu(10)
+        visible: stickersGrid.model.packName.length > 0
+
+        model: stickersModel
+        delegate: StickerDelegate {
+            stickerSource: filePath
+            width: stickersGrid.cellWidth
+            height: stickersGrid.cellHeight
+
+            onTriggered: {
+                StickersHistoryModel.add("%1/%2".arg(stickersGrid.model.packName).arg(fileName))
+                pickerRoot.stickerSelected(stickerSource)
+            }
+        }
+    }
+
+    GridView {
+        id: historyGrid
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: setsList.bottom
+        anchors.bottom: parent.bottom
+        clip: true
+        cellWidth: units.gu(10)
+        cellHeight: units.gu(10)
+        visible: stickersGrid.model.packName.length === 0
+
+        model: StickersHistoryModel
+
+        delegate: StickerDelegate {
+            stickerSource: "%1/stickers/%2".arg(dataLocation).arg(sticker)
+            width: stickersGrid.cellWidth
+            height: stickersGrid.cellHeight
+
+            onTriggered: {
+                StickersHistoryModel.add(sticker)
+                pickerRoot.stickerSelected(stickerSource)
+            }
+        }
+    }
+}

=== modified file 'src/qml/ThreadDelegate.qml'
--- src/qml/ThreadDelegate.qml	2015-10-23 20:41:36 +0000
+++ src/qml/ThreadDelegate.qml	2016-02-05 00:58:50 +0000
@@ -38,6 +38,7 @@
     property var displayedEventTextAttachments: displayedEvent ? displayedEvent.textMessageAttachments : eventTextAttachments
     property var displayedEventTimestamp: displayedEvent ? displayedEvent.timestamp : eventTimestamp
     property var displayedEventTextMessage: displayedEvent ? displayedEvent.textMessage : eventTextMessage
+    property QtObject presenceItem: delegateHelper.presenceItem
     property string groupChatLabel: {
         var firstRecipient
         if (unknownContact) {
@@ -58,6 +59,7 @@
         var imageCount = 0
         var videoCount = 0
         var contactCount = 0
+        var audioCount = 0
         var attachmentCount = 0
         for (var i = 0; i < displayedEventTextAttachments.length; i++) {
             if (startsWith(displayedEventTextAttachments[i].contentType, "text/plain")) {
@@ -69,9 +71,11 @@
             } else if (startsWith(displayedEventTextAttachments[i].contentType, "text/vcard") ||
                       startsWith(displayedEventTextAttachments[i].contentType, "text/x-vcard")) {
                 contactCount++
+            } else if (startsWith(displayedEventTextAttachments[i].contentType, "audio/")) {
+                audioCount++
             }
         }
-        attachmentCount = imageCount + videoCount + contactCount
+        attachmentCount = imageCount + videoCount + contactCount + audioCount
 
         if (imageCount > 0 && attachmentCount == imageCount) {
             return i18n.tr("Attachment: %1 image", "Attachments: %1 images").arg(imageCount)
@@ -82,6 +86,9 @@
         if (contactCount > 0 && attachmentCount == contactCount) {
             return i18n.tr("Attachment: %1 contact", "Attachments: %1 contacts").arg(contactCount)
         }
+        if (audioCount > 0 && attachmentCount == audioCount) {
+            return i18n.tr("Attachment: %1 audio clip", "Attachments: %1 audio clips").arg(audioCount)
+        }
         if (attachmentCount > 0) {
             return i18n.tr("Attachment: %1 file", "Attachments: %1 files").arg(attachmentCount)
         }
@@ -172,16 +179,36 @@
         fontSize: "small"
     }
 
+    Image {
+        id: protocolIcon
+        anchors {
+            top: time.bottom
+            topMargin: units.gu(1)
+            right: parent.right
+        }
+        height: units.gu(2)
+        width: units.gu(2)
+        visible: source !== ""
+        source: {
+            if (delegateHelper.presenceType != PresenceRequest.PresenceTypeUnknown
+                    && delegateHelper.presenceType != PresenceRequest.PresenceTypeUnset) {
+                return telepathyHelper.accountForId(delegateHelper.presenceAccountId).protocolInfo.icon
+            }
+            return ""
+        }
+    }
+
     UbuntuShape {
         id: unreadCountIndicator
         height: units.gu(2)
         width: height
         anchors {
-            top: time.bottom
-            topMargin: units.gu(1)
-            right: parent.right
-            rightMargin: units.gu(2)
+            top: avatar.top
+            topMargin: units.gu(-0.5)
+            left: avatar.left
+            leftMargin: units.gu(-0.5)
         }
+        z: 1
         visible: unreadCount > 0
         color: "#38b44a"
         Label {
@@ -232,6 +259,9 @@
         property alias contexts: phoneDetail.contexts
         property bool isUnknown: contactId === ""
         property string phoneNumberSubTypeLabel: ""
+        property alias presenceAccountId: presenceRequest.accountId
+        property alias presenceType: presenceRequest.type
+        property alias presenceItem: presenceRequest
         property string latestFilter: ""
         property var searchHistoryFilter
         property var searchHistoryFilterString: 'import Ubuntu.History 0.1; 
@@ -310,6 +340,35 @@
             }
         }
 
+        // FIXME: there is another instance of PresenceRequest in Messages.qml,
+        // we have to reuse the same instance when possible
+        PresenceRequest {
+            id: presenceRequest
+            accountId: {
+                // if this is a regular sms chat, try requesting the presence on
+                // a multimedia account
+                if (!telepathyHelper.ready) {
+                    return ""
+                }
+                var account = telepathyHelper.accountForId(model.accountId)
+                if (!account) {
+                    return ""
+                }
+                if (account.type == AccountEntry.PhoneAccount) {
+                    for (var i in telepathyHelper.accounts) {
+                        var tmpAccount = telepathyHelper.accounts[i]
+                        if (tmpAccount.type == AccountEntry.MultimediaAccount) {
+                            return tmpAccount.accountId
+                        }
+                    }
+                    return ""
+                }
+                return account.accountId
+            }
+            // we just request presence on 1-1 chats
+            identifier: !groupChat ? participant.identifier : ""
+        }
+
         function updateSubTypeLabel() {
             var subLabel = "";
             if (participant && participant.phoneNumber) {

=== added file 'src/qml/ThumbnailContact.qml'
--- src/qml/ThumbnailContact.qml	1970-01-01 00:00:00 +0000
+++ src/qml/ThumbnailContact.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2012-2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Contacts 0.1
+
+Item {
+    id: attachment
+
+    readonly property int contactsCount:vcardParser.contacts ? vcardParser.contacts.length : 0
+    property int index
+    property string filePath
+    property alias vcard: vcardParser
+    property string contactDisplayName: {
+        if (contactsCount > 0)  {
+            var contact = vcard.contacts[0]
+            if (contact.displayLabel.label && (contact.displayLabel.label != "")) {
+                return contact.displayLabel.label
+            } else if (contact.name) {
+                var contacFullName  = contact.name.firstName
+                if (contact.name.midleName) {
+                    contacFullName += " " + contact.name.midleName
+                }
+                if (contact.name.lastName) {
+                    contacFullName += " " + contact.name.lastName
+                }
+                return contacFullName
+            }
+            return i18n.tr("Unknown contact")
+        }
+        return ""
+    }
+    property string title: {
+        var result = attachment.contactDisplayName
+        if (attachment.contactsCount > 1) {
+            return result + " (+%1)".arg(attachment.contactsCount-1)
+        } else {
+            return result
+        }
+    }
+
+    signal pressAndHold()
+
+    height: units.gu(6)
+    width: textEntry.width
+
+    ContactAvatar {
+        id: avatar
+
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+            left: parent.left
+        }
+        contactElement: attachment.contactsCount === 1 ? attachment.vcard.contacts[0] : null
+        fallbackAvatarUrl: attachment.contactsCount === 1 ? "image://theme/contact" : "image://theme/contact-group"
+        fallbackDisplayName: attachment.contactsCount === 1 ? attachment.contactDisplayName : ""
+        width: height
+    }
+    Label {
+        id: label
+
+        anchors {
+            left: avatar.right
+            leftMargin: units.gu(1)
+            top: avatar.top
+            bottom: avatar.bottom
+            right: parent.right
+            rightMargin: units.gu(1)
+        }
+
+        verticalAlignment: Text.AlignVCenter
+        text: attachment.title
+        elide: Text.ElideMiddle
+        color: UbuntuColors.lightAubergine
+    }
+    MouseArea {
+        anchors.fill: parent
+        onPressAndHold: {
+            mouse.accept = true
+            attachment.pressAndHold()
+        }
+    }
+    VCardParser {
+        id: vcardParser
+
+        vCardUrl: attachment ? Qt.resolvedUrl(attachment.filePath) : ""
+    }
+}
+

=== added file 'src/qml/ThumbnailImage.qml'
--- src/qml/ThumbnailImage.qml	1970-01-01 00:00:00 +0000
+++ src/qml/ThumbnailImage.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+UbuntuShape {
+    id: thumbnail
+    property int index
+    property string filePath
+
+    signal pressAndHold()
+
+    width: childrenRect.width
+    height: childrenRect.height
+
+    image: Image {
+        id: avatarImage
+        width: units.gu(8)
+        height: units.gu(8)
+        sourceSize.height: height
+        sourceSize.width: width
+        fillMode: Image.PreserveAspectCrop
+        source: filePath
+        asynchronous: true
+    }
+    MouseArea {
+        anchors.fill: parent
+        onPressAndHold: {
+            mouse.accept = true
+            thumbnail.pressAndHold()
+        }
+    }
+}

=== added file 'src/qml/ThumbnailUnknown.qml'
--- src/qml/ThumbnailUnknown.qml	1970-01-01 00:00:00 +0000
+++ src/qml/ThumbnailUnknown.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+UbuntuShape {
+    id: thumbnail
+    property int index
+    property string filePath
+
+    signal pressAndHold()
+
+    width: units.gu(8)
+    height: units.gu(8)
+
+    Icon {
+        anchors.centerIn: parent
+        width: units.gu(6)
+        height: units.gu(6)
+        name: "attachment"
+    }
+    MouseArea {
+        anchors.fill: parent
+        onPressAndHold: {
+            mouse.accept = true
+            thumbnail.pressAndHold()
+        }
+    }
+}

=== added file 'src/qml/ThumbnailVideo.qml'
--- src/qml/ThumbnailVideo.qml	1970-01-01 00:00:00 +0000
+++ src/qml/ThumbnailVideo.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+import Ubuntu.Thumbnailer 0.1
+
+UbuntuShape {
+    id: thumbnail
+    property int index
+    property string filePath
+
+    signal pressAndHold()
+
+    onFilePathChanged: videoImage.source = "image://thumbnailer/" + filePath
+
+    width: childrenRect.width
+    height: childrenRect.height
+
+    image: Image {
+        id: videoImage
+
+        width: units.gu(8)
+        height: units.gu(8)
+        sourceSize.width: width
+        sourceSize.height: height
+        fillMode: Image.PreserveAspectCrop
+        asynchronous: true
+
+        onStatusChanged:  {
+            if (status === Image.Error) {
+                source = "image://theme/image-missing"
+            }
+        }
+    }
+
+    ActivityIndicator {
+        anchors.centerIn: parent
+        visible: running
+        running: videoImage.status != Image.Ready
+    }
+
+    Icon {
+        width: units.gu(3)
+        height: units.gu(3)
+        anchors.centerIn: parent
+        name: "media-playback-start"
+        color: "white"
+        visible: opacity > 0.0
+        opacity: videoImage.status == Image.Ready ? 0.8 : 0.0
+        Behavior on opacity { UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration} }
+    }
+
+    MouseArea {
+        anchors.fill: parent
+        onPressAndHold: {
+            mouse.accept = true
+            thumbnail.pressAndHold()
+        }
+    }
+}

=== added file 'src/qml/TransparentButton.qml'
--- src/qml/TransparentButton.qml	1970-01-01 00:00:00 +0000
+++ src/qml/TransparentButton.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.0
+import Ubuntu.Components 1.3
+
+Item {
+    id: button
+
+    width: sideBySide ? iconShape.width + spacing + label.width : iconShape.width
+    height: sideBySide ? iconShape.height : iconShape.height + label.height + spacing
+
+    property alias iconName: icon.name
+    property alias iconSource: icon.source
+    property alias iconColor: icon.color
+    property int iconSize: units.gu(2)
+    property alias iconRotation: icon.rotation
+    property alias text: label.text
+    property alias textSize: label.font.pixelSize
+    property alias textColor: label.color
+    property int spacing: 0
+    property bool sideBySide: false
+    property bool iconPulsate: false
+
+    property alias drag: mouseArea.drag
+
+    signal clicked()
+    signal pressed()
+    signal released()
+
+    Item {
+        id: iconShape
+        height: iconSize
+        width: iconSize
+        anchors {
+            left: parent.left
+            right: sideBySide ? undefined : parent.right
+            top: parent.top
+        }
+        Icon {
+            id: icon
+            anchors.centerIn: parent
+            height: iconSize
+            width: height
+            color: "gray"
+            Behavior on rotation {
+                UbuntuNumberAnimation { }
+            }
+            SequentialAnimation {
+                running: iconPulsate
+                loops: Animation.Infinite
+                NumberAnimation { target: icon; property: "scale"; from: 1; to: 0.7; duration: 1000; easing.type: Easing.InOutQuad }
+                NumberAnimation { target: icon; property: "scale"; from: 0.7; to: 1; duration: 1000; easing.type: Easing.InOutQuad }
+            }
+        }
+    }
+
+    MouseArea {
+        id: mouseArea
+        anchors {
+            fill: iconShape
+            margins: units.gu(-2)
+        }
+        onClicked: {
+            mouse.accepted = true
+            button.clicked()
+        }
+
+        onPressed: button.pressed()
+        onReleased: button.released()
+    }
+
+    Text {
+        id: label
+        color: "gray"
+        height: text !== "" ? paintedHeight : 0
+        anchors {
+            left: sideBySide ? iconShape.right : parent.left
+            right: sideBySide ? undefined : parent.right
+            bottom: sideBySide ? undefined : parent.bottom
+            verticalCenter: sideBySide ? iconShape.verticalCenter : undefined
+            leftMargin: sideBySide ? spacing : undefined
+        }
+        horizontalAlignment: sideBySide ? undefined : Text.AlignHCenter
+        verticalAlignment: sideBySide ? Text.AlignVCenter : Text.AlignBottom
+        font.family: "Ubuntu"
+        font.pixelSize: FontUtils.sizeToPixels("small")
+    }
+}

=== added file 'src/qml/assets/blue_bubble@27.png'
Binary files src/qml/assets/blue_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/blue_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/blue_bubble@27.sci'
--- src/qml/assets/blue_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/blue_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:135
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: blue_bubble@27.png
\ No newline at end of file

=== added file 'src/qml/assets/blue_bubble@27_1.png'
Binary files src/qml/assets/blue_bubble@27_1.png	1970-01-01 00:00:00 +0000 and src/qml/assets/blue_bubble@27_1.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/burn-after-read.svg'
--- src/qml/assets/burn-after-read.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/burn-after-read.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="96"
+   id="svg4874"
+   version="1.1"
+   inkscape:version="0.91+devel r"
+   viewBox="0 0 96 96.000001"
+   sodipodi:docname="burn-after-reading.svg">
+  <defs
+     id="defs4876" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="5.6199993"
+     inkscape:cx="-21.975098"
+     inkscape:cy="29.243754"
+     inkscape:document-units="px"
+     inkscape:current-layer="g4780"
+     showgrid="true"
+     showborder="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:snap-global="true"
+     inkscape:snap-others="false"
+     inkscape:snap-nodes="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid5451"
+       empspacing="8" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="8,-8.0000001"
+       id="guide4063" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,-8.0000001"
+       id="guide4065" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,88.000001"
+       id="guide4067" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,92.000001"
+       id="guide4069" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="104,4"
+       id="guide4071" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,8.0000001"
+       id="guide4073" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="92,-8.0000001"
+       id="guide4075" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="88,-8.0000001"
+       id="guide4077" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,84.000001"
+       id="guide4074" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="12,-8.0000001"
+       id="guide4076" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,12"
+       id="guide4078" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="84,-9.0000001"
+       id="guide4080" />
+    <sodipodi:guide
+       position="48,-8.0000001"
+       orientation="1,0"
+       id="guide4170" />
+    <sodipodi:guide
+       position="-8,48"
+       orientation="0,1"
+       id="guide4172" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata4879">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(67.857146,-78.50504)">
+    <g
+       transform="matrix(0,-1,-1,0,373.50506,516.50504)"
+       id="g4845"
+       style="display:inline">
+      <g
+         inkscape:export-ydpi="90"
+         inkscape:export-xdpi="90"
+         inkscape:export-filename="next01.png"
+         transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
+         id="g4778"
+         inkscape:label="Layer 1">
+        <g
+           transform="matrix(-1,0,0,1,575.99999,611)"
+           id="g4780"
+           style="display:inline">
+          <rect
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
+             id="rect4782"
+             width="96.037987"
+             height="96"
+             x="-438.00244"
+             y="345.36221"
+             transform="scale(-1,1)" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+             d="m 421.9962,417.39134 h -4.00158 v -44.02913 h 4.00158 z"
+             id="path4212"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+             d="m 409.99145,417.39134 h -4.00158 v -44.02913 h 4.00158 z"
+             id="path4210"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4214"
+             d="m 397.98669,417.39134 h -4.00158 v -43.98845 h 4.00158 z"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.00000048;marker:none;enable-background:accumulate"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999976;marker:none;enable-background:accumulate"
+             d="M 12,4 V 84 H 54 V 80 H 16 V 8 h 60 v 36 h 4 V 4 Z"
+             transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
+             id="path4315"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccccccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4172"
+             d="m 389.88737,369.3505 c -13.04831,6.49815 -27.59862,10.97763 -28.59981,11.28326 -0.11757,0.0435 -0.2405,0.0754 -0.35978,0.11522 -1.17278,0.39137 -2.42258,0.61324 -3.72522,0.61324 -5.10703,0 -9.46026,-3.25604 -11.13898,-7.82458 1.04868,1.04524 2.48763,1.69129 4.07726,1.69129 0.63686,0 1.24768,-0.10845 1.82105,-0.29979 0.0582,-0.0195 0.11854,-0.0344 0.17601,-0.0557 0.48947,-0.14942 7.60304,-2.33929 13.98221,-5.51616 0,-8.4e-4 -8.4e-4,-0.002 -0.002,-0.003 -4.8e-4,-8.4e-4 -0.002,-0.002 -0.003,-0.003 -8.4e-4,-8.4e-4 -0.003,-0.003 -0.003,-0.004 -8.4e-4,-8.3e-4 -8.4e-4,-0.003 -8.4e-4,-0.003 -6.96715,-3.3943 -14.14855,-5.55229 -14.14855,-5.55229 v 0.002 c -0.57305,-0.1911 -1.18459,-0.29588 -1.82102,-0.29588 -1.59042,0 -3.03137,0.6471 -4.08017,1.69323 1.67784,-4.57022 6.03267,-7.8275 11.14089,-7.8275 1.30182,0 2.5531,0.21454 3.72524,0.60542 v -0.004 c 0,0 14.68923,4.4136 28.94023,11.3565 0,0.002 8.3e-4,0.004 0.003,0.007 0.002,0.003 0.004,0.005 0.007,0.008 0.003,0.003 0.005,0.004 0.007,0.007 0.002,0.003 0.003,0.005 0.003,0.007 z"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;enable-background:accumulate" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

=== removed file 'src/qml/assets/conversation_bubble_arrow.png'
Binary files src/qml/assets/conversation_bubble_arrow.png	2015-08-17 17:39:45 +0000 and src/qml/assets/conversation_bubble_arrow.png	1970-01-01 00:00:00 +0000 differ
=== added file 'src/qml/assets/double_tick.svg'
--- src/qml/assets/double_tick.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/double_tick.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="52px" height="28px" viewBox="0 0 52 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
+    <!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
+    <title>path4041-9 copy + path4041-9 copy 2</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Messaging" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
+        <g id="Compose-SMS-empty-state-Copy-56" sketch:type="MSArtboardGroup" transform="translate(-948.000000, -512.000000)" fill="#FFFFFF">
+            <g id="tick" sketch:type="MSLayerGroup" transform="translate(941.000000, 499.000000)">
+                <g id="svg4874" sketch:type="MSShapeGroup">
+                    <g id="layer1">
+                        <g id="path4041-9-copy-+-path4041-9-copy-2" transform="translate(7.000000, 13.000000)">
+                            <path d="M15.3523623,21.1776844 L1.97756181,9.70609227 L0.120056,11.8063782 L15.3437936,27.824149 L39.756042,2.13611253 L38.5926018,0.824149 L15.3523623,21.1776844 Z" id="path4041-9-copy"></path>
+                            <path d="M27.3523623,21.1776844 L13.9775618,9.70609227 L12.120056,11.8063782 L27.3437936,27.824149 L51.756042,2.13611253 L50.5926018,0.824149 L27.3523623,21.1776844 Z" id="path4041-9-copy-2"></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file

=== added file 'src/qml/assets/face-smile-big-symbolic-2.svg'
--- src/qml/assets/face-smile-big-symbolic-2.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/face-smile-big-symbolic-2.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="96"
+   id="svg4874"
+   version="1.1"
+   inkscape:version="0.91+devel r"
+   viewBox="0 0 96 96.000001"
+   sodipodi:docname="face-smile-big-symbolic.svg">
+  <defs
+     id="defs4876" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8774396"
+     inkscape:cx="-93.086241"
+     inkscape:cy="47.64648"
+     inkscape:document-units="px"
+     inkscape:current-layer="g4780"
+     showgrid="true"
+     showborder="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     showguides="true"
+     inkscape:guide-bbox="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid5451"
+       empspacing="8" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="8,-8.0000001"
+       id="guide4063" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,-8.0000001"
+       id="guide4065" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,88.000001"
+       id="guide4067" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,92.000001"
+       id="guide4069" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="104,4"
+       id="guide4071" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,8.0000001"
+       id="guide4073" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="92,-8.0000001"
+       id="guide4075" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="88,-8.0000001"
+       id="guide4077" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,84.000001"
+       id="guide4074" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="12,-8.0000001"
+       id="guide4076" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,12"
+       id="guide4078" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="84,-9.0000001"
+       id="guide4080" />
+    <sodipodi:guide
+       position="48,-8.0000001"
+       orientation="1,0"
+       id="guide4170" />
+    <sodipodi:guide
+       position="-8,48"
+       orientation="0,1"
+       id="guide4172" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata4879">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(67.857146,-78.50504)">
+    <g
+       transform="matrix(0,-1,-1,0,373.50506,516.50504)"
+       id="g4845"
+       style="display:inline">
+      <g
+         inkscape:export-ydpi="90"
+         inkscape:export-xdpi="90"
+         inkscape:export-filename="next01.png"
+         transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
+         id="g4778"
+         inkscape:label="Layer 1">
+        <g
+           transform="matrix(-1,0,0,1,575.99999,611)"
+           id="g4780"
+           style="display:inline">
+          <rect
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
+             id="rect4782"
+             width="96.037987"
+             height="96"
+             x="-438.00244"
+             y="345.36221"
+             transform="scale(-1,1)" />
+          <path
+             style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             d="m 432,393.36133 c 0,23.17236 -18.83538,42 -42.01562,42 -23.18025,0 -42.01563,-18.82764 -42.01563,-42 0,-23.17236 18.83538,-42 42.01563,-42 23.18024,0 42.01562,18.82764 42.01562,42 z m -4.00195,0 c 0,-21.00932 -16.99476,-37.99805 -38.01367,-37.99805 -21.01892,0 -38.01563,16.98873 -38.01563,37.99805 0,21.00931 16.99671,37.99804 38.01563,37.99805 21.01891,0 38.01367,-16.98874 38.01367,-37.99805 z"
+             id="path4116"
+             inkscape:connector-curvature="0" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:64.01265717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+             d="m 404.99002,409.36281 1.50057,-8.00059 -13.50534,0 0,8.00059 z"
+             id="rect4175"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             sodipodi:nodetypes="ccccc"
+             inkscape:connector-curvature="0"
+             id="path4178"
+             d="m 404.99002,385.36222 1.50057,-7.99941 -13.50534,0 0,7.99941 z"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:64.01265717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#808080;fill-opacity:1;stroke:none;stroke-width:1.00019777px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 383.98169,414.37529 c -1.5268,-0.21155 -2.98748,-0.52576 -4.37873,-0.94375 -2.81692,-0.88674 -5.23617,-2.19676 -7.25967,-3.92985 -2.0236,-1.77325 -3.59002,-3.99099 -4.70116,-6.65098 -1.11104,-2.70036 -1.66756,-5.86393 -1.66756,-9.49118 0,-3.62725 0.55652,-6.77166 1.66756,-9.43164 1.11114,-2.70027 2.67756,-4.91499 4.70116,-6.64799 2.0235,-1.77334 4.44275,-3.0835 7.25967,-3.92986 1.39125,-0.4379 2.85193,-0.76678 4.37873,-0.98841 l 0,42.01366 z"
+             id="path4190"
+             inkscape:connector-curvature="0"
+             inkscape:transform-center-x="0.0068360001"
+             inkscape:transform-center-y="-9.0000001" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

=== added file 'src/qml/assets/green_bubble@27.png'
Binary files src/qml/assets/green_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/green_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/green_bubble@27.sci'
--- src/qml/assets/green_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/green_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:135
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: green_bubble@27.png

=== added file 'src/qml/assets/grey_bubble@27.png'
Binary files src/qml/assets/grey_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/grey_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/grey_bubble@27.sci'
--- src/qml/assets/grey_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/grey_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:135
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: grey_bubble@27.png
\ No newline at end of file

=== added file 'src/qml/assets/history.svg'
--- src/qml/assets/history.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/history.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="96"
+   id="svg4874"
+   version="1.1"
+   inkscape:version="0.91+devel r"
+   viewBox="0 0 96 96.000001"
+   sodipodi:docname="history.svg">
+  <defs
+     id="defs4876" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="8.7812488"
+     inkscape:cx="12.412809"
+     inkscape:cy="56.142342"
+     inkscape:document-units="px"
+     inkscape:current-layer="g4780"
+     showgrid="true"
+     showborder="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     showguides="true"
+     inkscape:guide-bbox="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid5451"
+       empspacing="8" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="8,-8.0000001"
+       id="guide4063" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,-8.0000001"
+       id="guide4065" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,88.000001"
+       id="guide4067" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,92.000001"
+       id="guide4069" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="104,4"
+       id="guide4071" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,8.0000001"
+       id="guide4073" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="92,-8.0000001"
+       id="guide4075" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="88,-8.0000001"
+       id="guide4077" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,84.000001"
+       id="guide4074" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="12,-8.0000001"
+       id="guide4076" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,12"
+       id="guide4078" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="84,-9.0000001"
+       id="guide4080" />
+    <sodipodi:guide
+       position="48,-8.0000001"
+       orientation="1,0"
+       id="guide4170" />
+    <sodipodi:guide
+       position="-8,48"
+       orientation="0,1"
+       id="guide4172" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata4879">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(67.857146,-78.50504)">
+    <g
+       transform="matrix(0,-1,-1,0,373.50506,516.50504)"
+       id="g4845"
+       style="display:inline">
+      <g
+         inkscape:export-ydpi="90"
+         inkscape:export-xdpi="90"
+         inkscape:export-filename="next01.png"
+         transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
+         id="g4778"
+         inkscape:label="Layer 1">
+        <g
+           transform="matrix(-1,0,0,1,575.99999,611)"
+           id="g4780"
+           style="display:inline">
+          <rect
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
+             id="rect4782"
+             width="96.037987"
+             height="96"
+             x="-438.00244"
+             y="345.36221"
+             transform="scale(-1,1)" />
+          <path
+             style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             d="m 420.87891,421.82617 c -13.02519,14.12639 -34.00962,17.61704 -50.90821,8.4668 -16.89858,-9.15024 -25.43667,-28.62704 -20.71484,-47.25 4.72183,-18.62296 21.51014,-31.68164 40.72852,-31.68164 l 0,4.00195 c -17.40844,0 -32.5749,11.79684 -36.85157,28.66406 -4.27667,16.86723 3.43743,34.45783 18.74414,42.7461 15.30671,8.28827 34.26279,5.13698 46.06055,-7.65821 l 2.94141,2.71094 z"
+             id="path4116"
+             inkscape:connector-curvature="0" />
+          <path
+             style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:8.00158199, 8.00158199;stroke-dashoffset:1.60031641;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             d="m 431.99414,394.00977 -0.0137,0.42968 -3.99805,-0.13281 0.0137,-0.42187 0,-0.008 -0.006,-1.35937 0,-0.0254 -0.0527,-1.31446 -0.002,-0.0254 -0.10352,-1.36328 -0.002,-0.01 -0.16016,-1.41016 -0.20312,-1.34961 -0.004,-0.0215 -0.041,-0.21875 3.93164,-0.74414 0.0449,0.24023 0.0117,0.0723 0.2207,1.45703 0.01,0.0723 0.16602,1.45899 0.008,0.0742 0.11133,1.46093 0.006,0.0723 0.0586,1.46094 0,0.0723 0.006,1.45899 z m -1.11914,9.00195 -0.0156,0.0703 -0.36133,1.4082 -0.0195,0.0684 -0.41211,1.39453 -0.0215,0.0703 -0.46289,1.3789 -0.0234,0.0703 -0.51367,1.36328 -0.0273,0.0684 -0.5625,1.3457 -0.0293,0.0664 -0.17969,0.38867 -3.63281,-1.67774 0.15039,-0.32226 0.0137,-0.0312 0.54493,-1.30274 0.004,-0.0117 0.48242,-1.28516 0.004,-0.0117 0.42773,-1.27734 0.40039,-1.35938 0.3418,-1.33789 0.0469,-0.2168 3.9082,0.85352 z m -5.52344,-23.56055 -0.25,-0.63476 -0.0117,-0.0312 -0.54492,-1.24805 -0.0156,-0.0312 -0.60352,-1.25586 -0.0137,-0.0254 -0.68359,-1.29297 -0.002,-0.004 -0.6914,-1.19531 -0.0176,-0.0293 -0.64453,-1.02343 3.38672,-2.12891 0.68359,1.08789 0.0391,0.0625 0.74414,1.28906 0.0371,0.0664 0.69922,1.32227 0.0332,0.0664 0.6543,1.35547 0.0312,0.0684 0.60547,1.38477 0.0273,0.0703 0.26172,0.66601 z m -1.52929,38.8086 -0.0449,0.0605 -0.90625,1.18164 -0.0469,0.0566 -0.95313,1.15235 -0.0488,0.0566 -1.00196,1.12304 -0.0527,0.0547 -1.04883,1.08789 -2.88086,-2.77539 1.02344,-1.06054 0.92773,-1.03711 0.0234,-0.0274 0.88477,-1.0664 0.0215,-0.0274 0.86719,-1.13086 0.0176,-0.0234 0.1875,-0.26563 3.26171,2.31445 z m -6.66993,-51.46875 -0.68554,-0.68946 -0.0215,-0.0215 -0.99219,-0.92774 -0.0195,-0.0176 -0.98828,-0.85938 -0.0117,-0.01 -1.11132,-0.89648 -0.008,-0.008 -1.04687,-0.78516 -0.0176,-0.0117 -0.94922,-0.66015 2.28125,-3.28711 0.9668,0.67187 0.0586,0.043 1.16406,0.87109 0.0566,0.0449 1.13086,0.91406 0.0566,0.0469 1.09766,0.95508 0.0547,0.0488 1.06445,0.99609 0.0527,0.0508 0.70507,0.71093 z m -12.49609,-8.47852 -0.86523,-0.35547 -0.0176,-0.006 -1.32813,-0.49023 -0.0254,-0.01 -1.29101,-0.41992 -1.30469,-0.375 -0.0156,-0.004 -1.39843,-0.3457 -0.0156,-0.004 -1.02734,-0.21289 0.81445,-3.91601 1.10156,0.22851 0.0703,0.0156 1.43164,0.35351 0.0703,0.0195 1.41211,0.40429 0.0703,0.0215 1.39258,0.45508 0.0703,0.0254 1.3711,0.5039 0.0684,0.0274 0.9336,0.38281 z"
+             id="ellipse4178"
+             inkscape:connector-curvature="0" />
+          <path
+             style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.5999999;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+             d="m 387.98242,367.36133 0,26.83008 20.5957,20.58593 2.82813,-2.83007 -19.42187,-19.41407 0,-25.17187 -4.00196,0 z"
+             id="path4180"
+             inkscape:connector-curvature="0" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

=== added file 'src/qml/assets/input-keyboard-symbolic.svg'
--- src/qml/assets/input-keyboard-symbolic.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/input-keyboard-symbolic.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="96"
+   id="svg4874"
+   version="1.1"
+   inkscape:version="0.91+devel r"
+   viewBox="0 0 96 96.000001"
+   sodipodi:docname="input-keyboard-symbolic.svg">
+  <defs
+     id="defs4876" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="7.0249992"
+     inkscape:cx="12.362986"
+     inkscape:cy="54.10675"
+     inkscape:document-units="px"
+     inkscape:current-layer="g4780"
+     showgrid="true"
+     showborder="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid5451"
+       empspacing="8" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="8,-8.0000001"
+       id="guide4063" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,-8.0000001"
+       id="guide4065" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,88.000001"
+       id="guide4067" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,92.000001"
+       id="guide4069" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="104,4"
+       id="guide4071" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,8.0000001"
+       id="guide4073" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="88,-8.0000001"
+       id="guide4077" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,84.000001"
+       id="guide4074" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="12,-8.0000001"
+       id="guide4076" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="84,-8.0000001"
+       id="guide4080" />
+    <sodipodi:guide
+       position="48,-8.0000001"
+       orientation="1,0"
+       id="guide4170" />
+    <sodipodi:guide
+       position="-8,48"
+       orientation="0,1"
+       id="guide4172" />
+    <sodipodi:guide
+       position="92,-8.0000001"
+       orientation="1,0"
+       id="guide4760" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata4879">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(67.857146,-78.50504)">
+    <g
+       transform="matrix(0,-1,-1,0,373.50506,516.50504)"
+       id="g4845"
+       style="display:inline">
+      <g
+         inkscape:export-ydpi="90"
+         inkscape:export-xdpi="90"
+         inkscape:export-filename="next01.png"
+         transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
+         id="g4778"
+         inkscape:label="Layer 1">
+        <g
+           transform="matrix(-1,0,0,1,575.99999,611)"
+           id="g4780"
+           style="display:inline">
+          <rect
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
+             id="rect4782"
+             width="96.037987"
+             height="96"
+             x="-438.00244"
+             y="345.36221"
+             transform="scale(-1,1)" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 407.99059,425.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             id="path4321"
+             inkscape:connector-curvature="0" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 379.97952,411.36222 0,-36 -8.00318,0 0,36 8.00318,0 z"
+             id="path4301"
+             inkscape:connector-curvature="0" />
+          <path
+             sodipodi:nodetypes="ccccccccc"
+             inkscape:connector-curvature="0"
+             id="path4297"
+             d="m 371.97588,349.36261 c -10.00396,0 -9.8873,3.91306 -10.00396,14 l 0,59.99961 c 0.11666,10.08694 0,14 10.00396,14 l 36.01466,0 c 10.00396,0 9.8873,-3.91306 10.00396,-14 l 0,-59.99961 c -0.11666,-10.08694 0,-14 -10.00396,-14 z"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#808080;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4165"
+             d="m 407.99059,411.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 407.99059,397.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             id="path4167"
+             inkscape:connector-curvature="0" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4169"
+             d="m 407.99059,383.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4171"
+             d="m 393.98503,418.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 393.98503,404.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             id="path4173"
+             inkscape:connector-curvature="0" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4175"
+             d="m 393.98503,390.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 393.98503,376.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             id="path4177"
+             inkscape:connector-curvature="0" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4179"
+             d="m 379.97951,425.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4181"
+             d="m 379.97947,369.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
+          <path
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
+             d="m 407.99059,369.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
+             id="path4185"
+             inkscape:connector-curvature="0" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

=== added file 'src/qml/assets/media_bubble@27.png'
Binary files src/qml/assets/media_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/media_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/media_bubble@27.sci'
--- src/qml/assets/media_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/media_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:135
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: media_bubble@27.png
\ No newline at end of file

=== added file 'src/qml/assets/red_bubble@27.png'
Binary files src/qml/assets/red_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/red_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/red_bubble@27.sci'
--- src/qml/assets/red_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/red_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:135
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: red_bubble@27.png
\ No newline at end of file

=== added file 'src/qml/assets/red_bubble@27_1.png'
Binary files src/qml/assets/red_bubble@27_1.png	1970-01-01 00:00:00 +0000 and src/qml/assets/red_bubble@27_1.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/single_tick.svg'
--- src/qml/assets/single_tick.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/single_tick.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="40px" height="28px" viewBox="0 0 40 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
+    <!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
+    <title>path4041-9</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Messaging" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
+        <g id="Compose-SMS-empty-state-Copy-54" sketch:type="MSArtboardGroup" transform="translate(-961.000000, -512.000000)" fill="#FFFFFF">
+            <g id="bubble-1" sketch:type="MSLayerGroup" transform="translate(535.000000, 373.500000)">
+                <g id="tick" transform="translate(426.000000, 138.500000)" sketch:type="MSShapeGroup">
+                    <g id="svg4874">
+                        <g id="layer1">
+                            <path d="M15.3523623,21.1776844 L1.97756181,9.70609227 L0.120056,11.8063782 L15.3437936,27.824149 L39.756042,2.13611253 L38.5926018,0.824149 L15.3523623,21.1776844 Z" id="path4041-9"></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file

=== added file 'src/qml/assets/stock_document.svg'
--- src/qml/assets/stock_document.svg	1970-01-01 00:00:00 +0000
+++ src/qml/assets/stock_document.svg	2016-02-05 00:58:50 +0000
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="96"
+   id="svg4874"
+   version="1.1"
+   inkscape:version="0.91+devel r"
+   viewBox="0 0 96 96.000001"
+   sodipodi:docname="stock_document.svg">
+  <defs
+     id="defs4876" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="8.7812489"
+     inkscape:cx="-0.89964805"
+     inkscape:cy="50.539495"
+     inkscape:document-units="px"
+     inkscape:current-layer="g4780"
+     showgrid="true"
+     showborder="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid5451"
+       empspacing="8" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="8,-8.0000001"
+       id="guide4063" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="4,-8.0000001"
+       id="guide4065" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,88.000001"
+       id="guide4067" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,92.000001"
+       id="guide4069" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="104,4"
+       id="guide4071" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,8.0000001"
+       id="guide4073" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="92,-8.0000001"
+       id="guide4075" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="88,-8.0000001"
+       id="guide4077" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-8,84.000001"
+       id="guide4074" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="12,-8.0000001"
+       id="guide4076" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="-5,12"
+       id="guide4078" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="84,-9.0000001"
+       id="guide4080" />
+    <sodipodi:guide
+       position="48,-8.0000001"
+       orientation="1,0"
+       id="guide4170" />
+    <sodipodi:guide
+       position="-8,48"
+       orientation="0,1"
+       id="guide4172" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata4879">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(67.857146,-78.50504)">
+    <g
+       transform="matrix(0,-1,-1,0,373.50506,516.50504)"
+       id="g4845"
+       style="display:inline">
+      <g
+         inkscape:export-ydpi="90"
+         inkscape:export-xdpi="90"
+         inkscape:export-filename="next01.png"
+         transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
+         id="g4778"
+         inkscape:label="Layer 1">
+        <g
+           transform="matrix(-1,0,0,1,575.99999,611)"
+           id="g4780"
+           style="display:inline">
+          <rect
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
+             id="rect4782"
+             width="96.037987"
+             height="96"
+             x="-438.00244"
+             y="345.36221"
+             transform="scale(-1,1)" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+             d="m 421.99616,417.39134 h -4.00158 v -48.02913 h 4.00158 z"
+             id="path4212"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+             d="m 409.99141,417.39134 h -4.00158 v -48.02913 h 4.00158 z"
+             id="path4210"
+             inkscape:connector-curvature="0"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4214"
+             d="m 397.98665,417.39134 h -4.00158 v -47.98476 h 4.00158 z"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.00000048;marker:none;enable-background:accumulate"
+             sodipodi:nodetypes="ccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;marker:none;enable-background:accumulate"
+             d="m 434.00085,429.36222 h -88.03482 v -47.38477 -3.61523 l 21.00831,-21 h 3.61666 63.40985 z m -4.00158,-4 v -64 h -60.02374 v 20 h -20.00792 v 44 z"
+             id="path4315"
+             sodipodi:nodetypes="ccccccccccccccc" />
+          <path
+             sodipodi:nodetypes="ccccc"
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.00000048;marker:none;enable-background:accumulate"
+             d="m 385.98189,417.39134 h -4.00158 v -47.98476 h 4.00158 z"
+             id="path4213"
+             inkscape:connector-curvature="0" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

=== added file 'src/qml/assets/white_bubble@27.png'
Binary files src/qml/assets/white_bubble@27.png	1970-01-01 00:00:00 +0000 and src/qml/assets/white_bubble@27.png	2016-02-05 00:58:50 +0000 differ
=== added file 'src/qml/assets/white_bubble@27.sci'
--- src/qml/assets/white_bubble@27.sci	1970-01-01 00:00:00 +0000
+++ src/qml/assets/white_bubble@27.sci	2016-02-05 00:58:50 +0000
@@ -0,0 +1,5 @@
+border.left:143
+border.top: 27
+border.bottom: 48
+border.right: 135
+source: white_bubble@27.png

=== added file 'src/qml/assets/white_bubble@27_1.png'
Binary files src/qml/assets/white_bubble@27_1.png	1970-01-01 00:00:00 +0000 and src/qml/assets/white_bubble@27_1.png	2016-02-05 00:58:50 +0000 differ
=== modified file 'src/qml/dateUtils.js'
--- src/qml/dateUtils.js	2014-08-26 18:51:33 +0000
+++ src/qml/dateUtils.js	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2013 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -71,3 +71,8 @@
 
     return text;
 }
+
+function formattedTime(time) {
+    var d = new Date(0, 0, 0, 0, 0, time)
+    return d.getHours() == 0 ? Qt.formatTime(d, "mm:ss") : Qt.formatTime(d, "h:mm:ss")
+}

=== modified file 'src/qml/messaging-app.qml'
--- src/qml/messaging-app.qml	2015-11-23 19:51:16 +0000
+++ src/qml/messaging-app.qml	2016-02-05 00:58:50 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2013 Canonical Ltd.
+ * Copyright 2012-2015 Canonical Ltd.
  *
  * This file is part of messaging-app.
  *
@@ -23,6 +23,7 @@
 import Ubuntu.Telephony 0.1
 import Ubuntu.Content 0.1
 import Ubuntu.History 0.1
+import "Stickers"
 
 MainView {
     id: mainView
@@ -38,6 +39,10 @@
     }
     property QtObject account: defaultPhoneAccount()
     property bool applicationActive: Qt.application.active
+    property alias mainStack: layout
+    property bool dualPanel: mainStack.columns > 1
+    property QtObject bottomEdge: null
+    property bool composingNewMessage: bottomEdge.status === BottomEdge.Committed
 
     activeFocusOnPress: false
 
@@ -57,7 +62,7 @@
         return null
     }
 
-    function showContactDetails(contact, contactListPage, contactsModel) {
+    function showContactDetails(currentPage, contact, contactListPage, contactsModel) {
         var initialProperties =  { "contactListPage": contactListPage,
                                    "model": contactsModel}
 
@@ -67,21 +72,24 @@
             initialProperties['contact'] = contact
         }
 
-        mainStack.push(Qt.resolvedUrl("MessagingContactViewPage.qml"),
-                       initialProperties)
-    }
-
-    function addNewContact(phoneNumber, contactListPage) {
-        mainStack.push(Qt.resolvedUrl("MessagingContactEditorPage.qml"),
-                       { "contactId": contactId,
-                         "addPhoneToContact": phoneNumber,
-                         "contactListPage": contactListPage })
-    }
-
-    function addPhoneToContact(contact, phoneNumber, contactListPage, contactsModel) {
+        mainStack.addFileToCurrentColumnSync(currentPage,
+                                         Qt.resolvedUrl("MessagingContactViewPage.qml"),
+                                         initialProperties)
+    }
+
+    function addNewContact(currentPage, phoneNumber, contactListPage) {
+        mainStack.addFileToCurrentColumnSync(currentPage,
+                                         Qt.resolvedUrl("MessagingContactEditorPage.qml"),
+                                         { "contactId": contactId,
+                                           "addPhoneToContact": phoneNumber,
+                                           "contactListPage": contactListPage })
+    }
+
+    function addPhoneToContact(currentPage, contact, phoneNumber, contactListPage, contactsModel) {
         if (contact === "") {
-            mainStack.push(Qt.resolvedUrl("NewRecipientPage.qml"),
-                           { "phoneToAdd": phoneNumber })
+            mainStack.addFileToCurrentColumnSync(currentPage,
+                                             Qt.resolvedUrl("NewRecipientPage.qml"),
+                                             { "phoneToAdd": phoneNumber })
         } else {
             var initialProperties = { "addPhoneToContact": phoneNumber,
                                       "contactListPage": contactListPage,
@@ -91,8 +99,9 @@
             } else {
                 initialProperties['contact'] = contact
             }
-            mainStack.push(Qt.resolvedUrl("MessagingContactViewPage.qml"),
-                           initialProperties)
+            mainStack.addFileToCurrentColumnSync(currentPage,
+                                             Qt.resolvedUrl("MessagingContactViewPage.qml"),
+                                             initialProperties)
         }
     }
 
@@ -118,6 +127,10 @@
         threadModel.removeThreads(threads);
     }
 
+    function showBottomEdgePage(properties) {
+        bottomEdge.commitWithProperties(properties)
+    }
+
     Connections {
         target: telepathyHelper
         // restore default bindings if any system settings changed
@@ -140,13 +153,14 @@
     Component.onCompleted: {
         i18n.domain = "messaging-app"
         i18n.bindtextdomain("messaging-app", i18nDirectory)
+        emptyStack()
     }
 
     Connections {
         target: telepathyHelper
         onSetupReady: {
             if (multiplePhoneAccounts && !telepathyHelper.defaultMessagingAccount &&
-                !settings.mainViewIgnoreFirstTimeDialog && mainStack.depth === 1) {
+                !settings.mainViewIgnoreFirstTimeDialog && mainPage.displayedThreadIndex < 0) {
                 PopupUtils.open(Qt.createComponent("Dialogs/NoDefaultSIMCardDialog.qml").createObject(mainView))
             }
         }
@@ -167,17 +181,26 @@
     Settings {
         id: settings
         category: "DualSim"
+        property bool messagesDontShowFileSizeWarning: false
         property bool messagesDontAsk: false
         property bool mainViewIgnoreFirstTimeDialog: false
     }
 
+    StickerPacksModel {
+        id: stickerPacksModel
+    }
+
+    StickersModel {
+        id: stickersModel
+    }
+
     Connections {
         target: ContentHub
         onShareRequested: {
             var properties = {}
             emptyStack()
             properties["sharedAttachmentsTransfer"] = transfer
-            mainStack.currentPage.showBottomEdgePage(Qt.resolvedUrl("Messages.qml"), properties)
+            mainView.showBottomEdgePage(properties)
         }
     }
 
@@ -194,20 +217,28 @@
         } else if (startsWith(contentType, "text/vcard") ||
                    startsWith(contentType, "text/x-vcard")) {
             return ContentType.Contacts
+        } else if (startsWith(contentType, "video/")) {
+            return ContentType.Videos
         }
         return ContentType.Unknown
     }
 
     function emptyStack() {
-        while (mainStack.depth !== 1 && mainStack.depth !== 0) {
-            mainStack.pop()
+        mainStack.removePage(mainPage)
+        layout.deleteInstances()
+        showEmptyState()
+    }
+
+    function showEmptyState() {
+        if (mainStack.columns > 1) {
+            layout.addComponentToNextColumnSync(mainStack.primaryPage, emptyStatePageComponent)
         }
     }
 
     function startNewMessage() {
         var properties = {}
         emptyStack()
-        mainStack.currentPage.showBottomEdgePage(Qt.resolvedUrl("Messages.qml"))
+        mainView.showBottomEdgePage(properties)
     }
 
     function startChat(identifiers, text, accountId) {
@@ -249,9 +280,26 @@
         if (typeof(accountId)!=='undefined') {
             properties["accountId"] = accountId
         }
+
         emptyStack()
-        mainStack.push(Qt.resolvedUrl("Messages.qml"), properties)
-    }
+        // FIXME: AdaptivePageLayout takes a really long time to create pages,
+        // so we create manually and push that
+        mainStack.addComponentToNextColumnSync(mainPage, messagesWithBottomEdge, properties)
+    }
+
+    InputInfo {
+        id: inputInfo
+    }
+
+    // WORKAROUND: Due the missing feature on SDK, they can not detect if
+    // there is a mouse attached to device or not. And this will cause the
+    // bootom edge component to not work correct on desktop.
+    Binding {
+        target:  QuickUtils
+        property: "mouseAttached"
+        value: inputInfo.hasMouse
+    }
+
 
     Connections {
         target: UriHandler
@@ -262,11 +310,60 @@
        }
     }
 
-
-    PageStack {
-        id: mainStack
-
-        objectName: "mainStack"
-        Component.onCompleted: mainStack.push(Qt.resolvedUrl("MainPage.qml"))
+    Component {
+        id: messagesWithBottomEdge
+
+        Messages {
+            id: messages
+            height: mainPage.height
+
+            Component.onCompleted: mainPage._messagesPage = messages
+            Loader {
+                id: messagesBottomEdgeLoader
+                active: mainView.dualPanel
+                sourceComponent: MessagingBottomEdge {
+                    id: messagesBottomEdge
+                    parent: messages
+                    hint.text: ""
+                    hint.height: 0
+                }
+            }
+        }
+    }
+
+    Component {
+        id: emptyStatePageComponent
+        Page {
+            id: emptyStatePage
+
+            EmptyState {
+                labelVisible: mainPage.isEmpty
+            }
+
+            header: PageHeader { }
+
+            Loader {
+                id: bottomEdgeLoader
+                sourceComponent: MessagingBottomEdge {
+                    parent: emptyStatePage
+                }
+            }
+        }
+    }
+
+    MessagingPageLayout {
+        id: layout
+        anchors.fill: parent
+        primaryPage: MainPage {
+            id: mainPage
+        }
+
+        onColumnsChanged: {
+            // we only have things to do here in case no thread is selected
+            if (mainPage.displayedThreadIndex < 0) {
+                layout.removePage(mainPage)
+                showEmptyState()
+            }
+        }
     }
 }

=== added file 'src/stickers-history-model.cpp'
--- src/stickers-history-model.cpp	1970-01-01 00:00:00 +0000
+++ src/stickers-history-model.cpp	2016-02-05 00:58:50 +0000
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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 "stickers-history-model.h"
+
+// Qt
+#include <QtCore/QDebug>
+#include <QtCore/QMutexLocker>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+
+#define CONNECTION_NAME "messaging-app-stickers-history"
+
+/*!
+    \class StickersHistoryModel
+    \brief List model that stores information about most recently used stickers
+
+    StickersHistoryModel is a list model that stores information about the most
+    recently used stickers.
+    Each sticker is simply identified by the sticker pack name plus the name of
+    the sticker image file itself.
+    Stickers are ordered by the time of last use, with the most recent first.
+    By default the model stores a rolling list of the 10 most recently used
+    stickers, though this number can be changed by setting the /a limit
+*/
+StickersHistoryModel::StickersHistoryModel(QObject* parent)
+    : QAbstractListModel(parent),
+      m_limit(10)
+{
+    m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);
+}
+
+StickersHistoryModel::~StickersHistoryModel()
+{
+    m_database.close();
+    m_database = QSqlDatabase();
+    QSqlDatabase::removeDatabase(CONNECTION_NAME);
+}
+
+void StickersHistoryModel::resetDatabase(const QString& databaseName)
+{
+    beginResetModel();
+    m_entries.clear();
+    m_database.close();
+    m_database.setDatabaseName(databaseName);
+    m_database.open();
+    createOrAlterDatabaseSchema();
+    endResetModel();
+    populateFromDatabase();
+}
+
+void StickersHistoryModel::createOrAlterDatabaseSchema()
+{
+    QMutexLocker ml(&m_dbMutex);
+    QSqlQuery query(m_database);
+    QString statement = QLatin1String("CREATE TABLE IF NOT EXISTS history "
+                                      "(sticker VARCHAR, mostRecentUse DATETIME);");
+    query.prepare(statement);
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+}
+
+void StickersHistoryModel::populateFromDatabase()
+{
+    QSqlQuery query(m_database);
+    QString statement = QLatin1String("SELECT sticker, mostRecentUse "
+                                      "FROM history ORDER BY mostRecentUse DESC;");
+    query.prepare(statement);
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+
+    int count = 0;
+    while (query.next()) {
+        HistoryEntry entry;
+        entry.sticker = query.value(0).toString();
+        entry.mostRecentUse = QDateTime::fromTime_t(query.value(1).toInt());
+        beginInsertRows(QModelIndex(), count, count);
+        m_entries.append(entry);
+        endInsertRows();
+        ++count;
+    }
+}
+
+QHash<int, QByteArray> StickersHistoryModel::roleNames() const
+{
+    static QHash<int, QByteArray> roles;
+    if (roles.isEmpty()) {
+        roles[Sticker] = "sticker";
+        roles[MostRecentUse] = "mostRecentUse";
+    }
+    return roles;
+}
+
+int StickersHistoryModel::rowCount(const QModelIndex& parent) const
+{
+    Q_UNUSED(parent);
+    return m_entries.count();
+}
+
+QVariant StickersHistoryModel::data(const QModelIndex& index, int role) const
+{
+    if (!index.isValid()) {
+        return QVariant();
+    }
+    const HistoryEntry& entry = m_entries.at(index.row());
+    switch (role) {
+    case Sticker:
+        return entry.sticker;
+    case MostRecentUse:
+        return entry.mostRecentUse;
+    default:
+        return QVariant();
+    }
+}
+
+const QString StickersHistoryModel::databasePath() const
+{
+    return m_database.databaseName();
+}
+
+int StickersHistoryModel::limit() const
+{
+    return m_limit;
+}
+
+void StickersHistoryModel::setLimit(int limit)
+{
+    if (limit != m_limit) {
+        m_limit = limit;
+        Q_EMIT limitChanged();
+        removeExcessRows();
+    }
+}
+
+void StickersHistoryModel::setDatabasePath(const QString& path)
+{
+    if (path != databasePath()) {
+        if (path.isEmpty()) {
+            resetDatabase(":memory:");
+        } else {
+            resetDatabase(path);
+        }
+        Q_EMIT databasePathChanged();
+    }
+}
+
+int StickersHistoryModel::getEntryIndex(const QString& sticker) const
+{
+    for (int i = 0; i < m_entries.count(); ++i) {
+        if (m_entries.at(i).sticker == sticker) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+/*!
+    Add an entry to the model.
+
+    If an entry for the same sticker already exists, it is updated and placed
+    first in the model. Otherwise a new entry is created and added at the
+    begining of the model.
+    If the new row count exceeds the limit, excess rows are purged.
+*/
+void StickersHistoryModel::add(const QString& sticker)
+{
+    if (sticker.isEmpty()) {
+        return;
+    }
+    QDateTime now = QDateTime::currentDateTime();
+    int index = getEntryIndex(sticker);
+
+    if (index == -1) {
+        HistoryEntry entry;
+        entry.sticker = sticker;
+        entry.mostRecentUse = now;
+        beginInsertRows(QModelIndex(), 0, 0);
+        m_entries.prepend(entry);
+        endInsertRows();
+        insertNewEntryInDatabase(entry);
+        Q_EMIT rowCountChanged();
+        removeExcessRows();
+    } else {
+        HistoryEntry entry;
+        if (index > 0) {
+          beginMoveRows(QModelIndex(), index, index, QModelIndex(), 0);
+        }
+        entry = m_entries.takeAt(index);
+        entry.mostRecentUse = now;
+        m_entries.prepend(entry);
+        if (index > 0) {
+          endMoveRows();
+        }
+        QVector<int> roles;
+        roles << MostRecentUse;
+        Q_EMIT dataChanged(this->index(0), this->index(0), roles);
+        updateExistingEntryInDatabase(m_entries.first());
+    }
+}
+
+void StickersHistoryModel::removeExcessRows()
+{
+    if (m_limit < rowCount()) {
+        beginRemoveRows(QModelIndex(), m_limit, rowCount() - 1);
+        for (int i = rowCount() - 1; i >= m_limit; i--) {
+            HistoryEntry item = m_entries.takeAt(i);
+            removeEntryFromDatabase(item.sticker);
+        }
+        endRemoveRows();
+        Q_EMIT rowCountChanged();
+    }
+}
+
+void StickersHistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry)
+{
+    QMutexLocker ml(&m_dbMutex);
+    QSqlQuery query(m_database);
+    static QString statement = QLatin1String("INSERT INTO history (sticker, mostRecentUse) "
+                                             "VALUES (?, ?);");
+    query.prepare(statement);
+    query.addBindValue(entry.sticker);
+    query.addBindValue(entry.mostRecentUse.toTime_t());
+
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+}
+
+void StickersHistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry)
+{
+    QMutexLocker ml(&m_dbMutex);
+    QSqlQuery query(m_database);
+    static QString statement = QLatin1String("UPDATE history SET mostRecentUse=?"
+                                             " WHERE sticker=?;");
+    query.prepare(statement);
+    query.addBindValue(entry.mostRecentUse.toTime_t());
+    query.addBindValue(entry.sticker);
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+}
+
+void StickersHistoryModel::removeEntryFromDatabase(const QString& sticker)
+{
+    QMutexLocker ml(&m_dbMutex);
+    QSqlQuery query(m_database);
+    static QString statement = QLatin1String("DELETE FROM history WHERE sticker=?;");
+    query.prepare(statement);
+    query.addBindValue(sticker);
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+}
+
+void StickersHistoryModel::clearAll()
+{
+    if (!m_entries.isEmpty()) {
+        beginResetModel();
+        m_entries.clear();
+        endResetModel();
+        clearDatabase();
+        Q_EMIT rowCountChanged();
+    }
+}
+
+void StickersHistoryModel::clearDatabase()
+{
+    QMutexLocker ml(&m_dbMutex);
+    QSqlQuery query(m_database);
+    QString statement = QLatin1String("DELETE FROM history;");
+    query.prepare(statement);
+    if (!query.exec()) {
+      qWarning() << "Query failed" << query.lastError();
+    }
+}
+
+QVariantMap StickersHistoryModel::get(int i) const
+{
+    QVariantMap item;
+    QHash<int, QByteArray> roles = roleNames();
+
+    QModelIndex modelIndex = index(i, 0);
+    if (modelIndex.isValid()) {
+        Q_FOREACH(int role, roles.keys()) {
+            QString roleName = QString::fromUtf8(roles.value(role));
+            item.insert(roleName, data(modelIndex, role));
+        }
+    }
+    return item;
+}

=== added file 'src/stickers-history-model.h'
--- src/stickers-history-model.h	1970-01-01 00:00:00 +0000
+++ src/stickers-history-model.h	2016-02-05 00:58:50 +0000
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+#ifndef __STICKERS_HISTORY_MODEL_H__
+#define __STICKERS_HISTORY_MODEL_H__
+
+// Qt
+#include <QtCore/QAbstractListModel>
+#include <QtCore/QDateTime>
+#include <QtCore/QList>
+#include <QtCore/QMutex>
+#include <QtCore/QString>
+#include <QtSql/QSqlDatabase>
+
+class StickersHistoryModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged)
+    Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
+    Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged)
+
+    Q_ENUMS(Roles)
+
+public:
+    StickersHistoryModel(QObject* parent=0);
+    ~StickersHistoryModel();
+
+    enum Roles {
+        Sticker = Qt::UserRole + 1,
+        MostRecentUse
+    };
+
+    // reimplemented from QAbstractListModel
+    QHash<int, QByteArray> roleNames() const;
+    int rowCount(const QModelIndex& parent=QModelIndex()) const;
+    QVariant data(const QModelIndex& index, int role) const;
+
+    const QString databasePath() const;
+    void setDatabasePath(const QString& path);
+    int limit() const;
+    void setLimit(int limit);
+
+    Q_INVOKABLE void add(const QString& sticker);
+    Q_INVOKABLE void clearAll();
+    Q_INVOKABLE QVariantMap get(int index) const;
+
+Q_SIGNALS:
+    void databasePathChanged() const;
+    void rowCountChanged() const;
+    void limitChanged() const;
+
+protected:
+    struct HistoryEntry {
+        QString sticker;
+        QDateTime mostRecentUse;
+    };
+    QList<HistoryEntry> m_entries;
+    int getEntryIndex(const QString& sticker) const;
+    void updateExistingEntryInDatabase(const HistoryEntry& entry);
+
+private:
+    QMutex m_dbMutex;
+    QSqlDatabase m_database;
+    int m_limit;
+
+    void resetDatabase(const QString& databaseName);
+    void createOrAlterDatabaseSchema();
+    void populateFromDatabase();
+    void insertNewEntryInDatabase(const HistoryEntry& entry);
+    void removeEntryFromDatabase(const QString& sticker);
+    void clearDatabase();
+    void removeExcessRows();
+};
+
+#endif // __STICKERS_HISTORY_MODEL_H__

=== modified file 'tests/autopilot/messaging_app/emulators.py'
--- tests/autopilot/messaging_app/emulators.py	2015-11-04 19:57:45 +0000
+++ tests/autopilot/messaging_app/emulators.py	2016-02-05 00:58:50 +0000
@@ -78,14 +78,14 @@
 
     def click_header_action(self, action):
         """Click the action 'action' on the header"""
-        self.get_header().click_action_button(action)
+        action = self.wait_select_single(objectName='%s_button' % action)
+        self.pointing_device.click_object(action)
 
     # messages page
     def get_messages_page(self):
         """Return messages with objectName messagesPage"""
 
-        return self.wait_select_single("Messages", objectName="messagesPage",
-                                       active=True)
+        return self.wait_select_single("Messages", objectName="messagesPage")
 
     def get_newmessage_textfield(self):
         """Return TextField with objectName newPhoneNumberField"""
@@ -116,40 +116,12 @@
 
         return self.get_messages_page().select_single(objectName='sendButton')
 
-    def get_toolbar_back_button(self):
-        """Return toolbar button with objectName back_toolbar_button"""
-
-        return self.select_single(
-            objectName='back_toolbar_button'
-        )
-
-    def get_toolbar_select_messages_button(self):
-        """Return toolbar button with objectName selectMessagesButton"""
-
-        return self.select_single(
-            objectName='selectMessagesButton'
-        )
-
     def get_contact_list_view(self):
         """Returns the ContactListView object"""
         return self.select_single(
             objectName='newRecipientList'
         )
 
-    def get_toolbar_contact_profile_button(self):
-        """Return toolbar button with objectName contactProfileButton"""
-
-        return self.select_single(
-            objectName='contactProfileButton'
-        )
-
-    def get_toolbar_contact_call_button(self):
-        """Return toolbar button with objectName contactCallButton"""
-
-        return self.select_single(
-            objectName='contactCallButton'
-        )
-
     def get_dialog_buttons(self, visible=True):
         """Return DialogButtons
 
@@ -213,7 +185,7 @@
 
     def start_new_message(self):
         """Reveal the bottom edge page to start composing a new message"""
-        self.get_main_page().reveal_bottom_edge_page()
+        self.reveal_bottom_edge_page()
 
     def enable_messages_selection_mode(self):
         """Enable the selection mode on the messages page by pressing and
@@ -338,38 +310,30 @@
 
     def open_settings_page(self):
         self.click_threads_header_settings()
-        return self.wait_select_single(SettingsPage)
+        settings_page = self.wait_select_single(SettingsPage)
+        settings_page.active.waitFor(True)
+        return settings_page
 
     def get_swipe_item_demo(self):
         return self.wait_select_single(
             'SwipeItemDemo', objectName='swipeItemDemo', parentActive=True)
 
-
-class PageWithBottomEdge(MainView):
-    """An emulator class that makes it easy to interact with the bottom edge
-       swipe page"""
-    def __init__(self, *args):
-        super(PageWithBottomEdge, self).__init__(*args)
-
     def reveal_bottom_edge_page(self):
         """Bring the bottom edge page to the screen"""
-        self.bottomEdgePageLoaded.wait_for(True)
         try:
-            action_item = self.wait_select_single(objectName='bottomEdgeTip')
-            start_x = (action_item.globalRect.x +
-                       (action_item.globalRect.width * 0.5))
-            start_y = (action_item.globalRect.y +
-                       (action_item.height * 0.5))
+            start_x = (self.globalRect.x +
+                       (self.globalRect.width * 0.5))
+            start_y = (self.globalRect.y + self.height)
             stop_y = start_y - (self.height * 0.7)
             self.pointing_device.drag(start_x, start_y,
                                       start_x, stop_y, rate=2)
-            self.isReady.wait_for(True)
+            self.composingNewMessage.wait_for(True)
         except StateNotFoundError:
             logger.error('BottomEdge element not found.')
             raise
 
 
-class MainPage(PageWithBottomEdge):
+class MainPage(MainView):
     """Autopilot helper for the Main Page."""
 
     def get_thread_count(self):

=== modified file 'tests/qml/CMakeLists.txt'
--- tests/qml/CMakeLists.txt	2015-08-13 18:34:55 +0000
+++ tests/qml/CMakeLists.txt	2016-02-05 00:58:50 +0000
@@ -1,33 +1,44 @@
-find_program(QMLTESTRUNNER_BIN
-    NAMES qmltestrunner
-    PATHS /usr/lib/*/qt5/bin
-    NO_DEFAULT_PATH
-)
+find_package(Qt5Core REQUIRED)
+find_package(Qt5Qml REQUIRED)
+find_package(Qt5Quick REQUIRED)
+find_package(Qt5QuickTest REQUIRED)
+find_package(Qt5Sql REQUIRED)
+
+set(XVFB_COMMAND)
 
 find_program(XVFB_RUN_BIN
     NAMES xvfb-run
 )
 
-macro(DECLARE_QML_TEST TST_NAME TST_QML_FILE)
-    add_test(NAME ${TST_NAME}
-        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-        COMMAND ${XVFB_RUN_BIN} -a -s "-screen 0 1024x768x24" ${QMLTESTRUNNER_BIN} -import ${qml_BINARY_DIR} -input ${CMAKE_CURRENT_SOURCE_DIR}/${TST_QML_FILE}
-    )
-endmacro()
-
-if(QMLTESTRUNNER_BIN AND XVFB_RUN_BIN)
-    declare_qml_test("message_bubble" tst_MessageBubble.qml)
-    declare_qml_test("messages_view" tst_MessagesView.qml)
+if(XVFB_RUN_BIN)
+    set(XVFB_COMMAND ${XVFB_RUN_BIN} -s "-screen 0 1024x768x24" -a)
 else()
-    if (NOT QMLTESTRUNNER_BIN)
-        message(WARNING "Qml tests disabled: qmltestrunner not found")
-    else()
-        message(WARNING "Qml tests disabled: xvfb-run not found")
-    endif()
+    message(WARNING "Qml tests disabled: xvfb-run not found")
 endif()
 
-set(QML_TST_FILES
-    tst_MessageBubble.qml
-    tst_MessagesView.qml
-)
-add_custom_target(tst_QmlFiles ALL SOURCES ${QML_TST_FILES})
+set(TEST tst_QmlTests)
+
+set(SOURCES
+    ${messaging-app_SOURCE_DIR}/src/audiorecorder.cpp
+    ${messaging-app_SOURCE_DIR}/src/fileoperations.cpp
+    ${messaging-app_SOURCE_DIR}/src/stickers-history-model.cpp
+    tst_QmlTests.cpp
+)
+
+add_executable(${TEST} ${SOURCES})
+qt5_use_modules(${TEST} Core Multimedia Qml Quick QuickTest Sql)
+
+include_directories(
+    ${messaging-app_SOURCE_DIR}/src
+    ${CMAKE_CURRENT_BINARY_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+add_test(${TEST} ${XVFB_COMMAND} ${CMAKE_CURRENT_BINARY_DIR}/${TEST}
+         -input ${CMAKE_CURRENT_SOURCE_DIR}
+         -import ${CMAKE_BINARY_DIR}/src
+         -import ${UNITY8_QML_PATH})
+
+# make qml files visible in QtCreator
+file(GLOB_RECURSE NON_COMPILED_FILES *.qml)
+add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_FILES})

=== added directory 'tests/qml/data'
=== added file 'tests/qml/data/sample.mp4'
Binary files tests/qml/data/sample.mp4	1970-01-01 00:00:00 +0000 and tests/qml/data/sample.mp4	2016-02-05 00:58:50 +0000 differ
=== added file 'tests/qml/data/sample.ogg'
Binary files tests/qml/data/sample.ogg	1970-01-01 00:00:00 +0000 and tests/qml/data/sample.ogg	2016-02-05 00:58:50 +0000 differ
=== added file 'tests/qml/data/sample.png'
Binary files tests/qml/data/sample.png	1970-01-01 00:00:00 +0000 and tests/qml/data/sample.png	2016-02-05 00:58:50 +0000 differ
=== added file 'tests/qml/tst_MMSDelegate.qml'
--- tests/qml/tst_MMSDelegate.qml	1970-01-01 00:00:00 +0000
+++ tests/qml/tst_MMSDelegate.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.2
+import QtTest 1.0
+import Ubuntu.Test 0.1
+
+import '../../src/qml/'
+
+Item {
+    id: root
+
+    width: units.gu(40)
+    height: units.gu(40)
+
+    MMSDelegate {
+        id: mmsDelegate
+        objectName: "mmsDelegate"
+
+        function startsWith(str, prefix) {
+            return str.toLowerCase().slice(0, prefix.length) === prefix.toLowerCase();
+        }
+
+        anchors.fill: parent
+
+        messageData: {
+            "participants": [],
+            "sender": {"alias": ""},
+            "textMessageAttachments": [],
+        }
+    }
+
+    UbuntuTestCase {
+        id: mmsImageDelegateTestCase
+        name: 'mmsImageDelegateTestCase'
+
+        when: windowShown
+
+        function test_load_image() {
+            mmsDelegate.messageData = {
+                "newEvent": false,
+                "participants": [],
+                "sender": {"alias": ""},
+                "senderId": "self",
+                "textMessage": "Message Delegate QML Test",
+                "textMessageAttachments": [
+                    {
+                        "contentType": "image/png",
+                        "filePath": Qt.resolvedUrl("./data/sample.png")
+                    }
+                ],
+                "textMessageStatus": 1,
+                "textReadTimestamp": new Date(),
+                "timestamp": new Date()
+            }
+
+            var image = findChild(mmsDelegate, "imageAttachment")
+            verify(image != null)
+            waitForRendering(image)
+            verify(image.source != "image://theme/image-missing")
+        }
+
+        function test_load_invalid_path() {
+            mmsDelegate.messageData = {
+                "newEvent": false,
+                "participants": [],
+                "sender": {"alias": ""},
+                "senderId": "self",
+                "textMessage": "Message Delegate QML Test",
+                "textMessageAttachments": [
+                    {
+                        "contentType": "image/png",
+                        "filePath": "/wrong/path/file.png"
+                    }
+                ],
+                "textMessageStatus": 1,
+                "textReadTimestamp": new Date(),
+                "timestamp": new Date()
+            }
+
+            var image = findChild(mmsDelegate, "imageAttachment")
+            verify(image != null)
+            waitForRendering(image)
+            compare(image.source, "image://theme/image-missing")
+        }
+    }
+
+    UbuntuTestCase {
+        id: mmsVideoDelegateTestCase
+        name: 'mmsVideoDelegateTestCase'
+
+        when: windowShown
+            
+        function test_load_video() {
+            mmsDelegate.messageData = {
+                "newEvent": false,
+                "participants": [],
+                "sender": {"alias": ""},
+                "senderId": "self",
+                "textMessage": "Message Delegate QML Test",
+                "textMessageAttachments": [
+                    {
+                        "contentType": "video/mp4",
+                        "filePath": Qt.resolvedUrl("./data/sample.mp4")
+                    }
+                ],
+                "textMessageStatus": 1,
+                "textReadTimestamp": new Date(),
+                "timestamp": new Date()
+            }
+
+            var video = findChild(mmsDelegate, "videoAttachment")
+            verify(video != null)
+            waitForRendering(video)
+            verify(video.source != "image://theme/image-missing")
+
+            var icon = findChild(mmsDelegate, "playbackStartIcon")
+            verify(icon != null)
+            waitForRendering(icon)
+            verify(icon.visible)
+        }
+
+        function test_load_invalid_path() {
+            skip("image://thumbnailer is not reporting an error for wrong file path")
+            mmsDelegate.messageData = {
+                "newEvent": false,
+                "participants": [],
+                "sender": {"alias": ""},
+                "senderId": "self",
+                "textMessage": "Message Delegate QML Test",
+                "textMessageAttachments": [
+                    {
+                        "contentType": "video/mp4",
+                        "filePath": "/wrong/path/file.mp4"
+                    }
+                ],
+                "textMessageStatus": 1,
+                "textReadTimestamp": new Date(),
+                "timestamp": new Date()
+            }
+
+            var video = findChild(mmsDelegate, "videoAttachment")
+            verify(video != null)
+            waitForRendering(video)
+            compare(video.source, "image://theme/image-missing")
+        }
+    }
+
+    UbuntuTestCase {
+        id: mmsAudioDelegateTestCase
+        name: 'mmsAudioDelegateTestCase'
+
+        when: windowShown
+            
+        function test_load_audio() {
+            mmsDelegate.messageData = {
+                "newEvent": false,
+                "participants": [],
+                "sender": {"alias": ""},
+                "senderId": "self",
+                "textMessage": "Message Delegate QML Test",
+                "textMessageAttachments": [
+                    {
+                        "contentType": "audio/ogg",
+                        "filePath": Qt.resolvedUrl("./data/sample.ogg")
+                    }
+                ],
+                "textMessageStatus": 1,
+                "textReadTimestamp": new Date(),
+                "timestamp": new Date()
+            }
+
+            var playButton = findChild(mmsDelegate, "playButton")
+            verify(playButton != null)
+            tryCompare(playButton, "visible", true)
+        }
+    }
+}

=== modified file 'tests/qml/tst_MessageBubble.qml'
--- tests/qml/tst_MessageBubble.qml	2014-08-27 17:50:35 +0000
+++ tests/qml/tst_MessageBubble.qml	2016-02-05 00:58:50 +0000
@@ -73,20 +73,20 @@
         function test_incomingMessageBubbleMustUseIncomingSource() {
             var incomingMessageBubble = findChild(
                 root, 'incomingMessageBubble');
-            compare(incomingMessageBubble.color, "#ffffff")
+            compare(incomingMessageBubble.color, "white")
         }
 
         function test_outgoingMessageBubbleMustUseOutgoingSource() {
             var outgoingMessageBubble = findChild(
                 root, 'outgoingMessageBubble');
-            compare(outgoingMessageBubble.color, "#3fb24f")
+            compare(outgoingMessageBubble.color, "green")
         }
 
         function test_changeIncomingMustUpdateSource() {
             var changeIncomingMessageBubble = findChild(
                 root, 'changeIncomingMessageBubble');
             changeIncomingMessageBubble.messageIncoming = false;
-            compare(changeIncomingMessageBubble.color, "#3fb24f")
+            compare(changeIncomingMessageBubble.color, "green")
         }
     }
 }

=== modified file 'tests/qml/tst_MessagesView.qml'
--- tests/qml/tst_MessagesView.qml	2015-11-19 00:34:45 +0000
+++ tests/qml/tst_MessagesView.qml	2016-02-05 00:58:50 +0000
@@ -129,16 +129,21 @@
             var senderId = "1234567"
             var stack = findChild(mainViewLoader, "mainStack")
             tryCompare(mainViewLoader.item, 'applicationActive', true)
-            tryCompare(stack, 'depth', 1)
             // if messaging-app has no account set, it will not try to get the thread from history
             // and instead will generate the list of participants, take advantage of that
             var account = mainViewLoader.item.account
             mainViewLoader.item.account = null
             mainViewLoader.item.startChat(senderId, "")
             mainViewLoader.item.account = account
-            tryCompare(stack, 'depth', 2)
-            mainViewLoader.item.applicationActive = false
-            var messageList = findChild(mainViewLoader, "messageList")
+            var messageList
+            while (true) {
+                messageList = findChild(mainViewLoader, "messageList")
+                if (messageList) {
+                    break
+                }
+                wait(200)
+            }
+
             messageList.listModel = messagesModel
             tryCompare(messageList, 'count', 2)
             compare(messageAcknowledgeSpy.count, 0)

=== added file 'tests/qml/tst_PreviewerImage.qml.disabled'
--- tests/qml/tst_PreviewerImage.qml.disabled	1970-01-01 00:00:00 +0000
+++ tests/qml/tst_PreviewerImage.qml.disabled	2016-02-05 00:58:50 +0000
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.2
+import QtTest 1.0
+import Ubuntu.Test 0.1
+
+import '../../src/qml/MMS'
+
+Item {
+    id: root
+
+    width: units.gu(40)
+    height: units.gu(40)
+
+    PreviewerImage {
+        id: previewerImage
+        objectName: "previewerImage"
+
+        property var application: QtObject {
+            property bool fullscreen: false
+        }
+
+        anchors.fill: parent
+
+        attachment: {
+            "contentType": "image/png",
+            "filePath": Qt.resolvedUrl("./data/sample.png")
+        }
+    }
+
+    UbuntuTestCase {
+        id: previewerImageTestCase
+        name: 'peviewerImageTestCase'
+
+        when: windowShown
+
+        function test_load_image() {
+            var activityIndicator = findChild(previewerImage, "imageActivityIndicator")
+            verify(activityIndicator != null)
+            tryCompare(activityIndicator, "visible", false)
+
+            var thumbnail = findChild(previewerImage, "thumbnailImage")
+            verify(thumbnail != null)
+            tryCompare(thumbnail, "opacity", 1.0)
+
+            var highRes = findChild(previewerImage, "highResolutionImage")
+            verify(highRes != null)
+            compare(highRes.source, "")
+        }
+
+        function test_zoom_in_out() {
+            var activityIndicator = findChild(previewerImage, "imageActivityIndicator")
+            verify(activityIndicator != null)
+            tryCompare(activityIndicator, "visible", false)
+
+            var thumbnail = findChild(previewerImage, "thumbnailImage")
+            verify(thumbnail != null)
+            tryCompare(thumbnail, "opacity", 1.0)
+
+            var highRes = findChild(previewerImage, "highResolutionImage")
+            verify(highRes != null)
+            compare(highRes.source, "")
+
+            mouseDoubleClick(thumbnail)
+            verify(highRes.source !== "")
+
+            mouseDoubleClick(thumbnail)
+            compare(highRes.source, "")
+        }
+
+        function test_toggle_fullscreen() {
+            var activityIndicator = findChild(previewerImage, "imageActivityIndicator")
+            verify(activityIndicator != null)
+            tryCompare(activityIndicator, "visible", false)
+
+            var thumbnail = findChild(previewerImage, "thumbnailImage")
+            verify(thumbnail != null)
+
+            verify(previewerImage.application.fullscreen)
+            mouseClick(thumbnail)
+            tryCompare(previewerImage.application, "fullscreen", false)
+        }
+    }
+}

=== added file 'tests/qml/tst_PreviewerVideo.qml.disabled'
--- tests/qml/tst_PreviewerVideo.qml.disabled	1970-01-01 00:00:00 +0000
+++ tests/qml/tst_PreviewerVideo.qml.disabled	2016-02-05 00:58:50 +0000
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.2
+import QtTest 1.0
+import Ubuntu.Content 0.1
+import Ubuntu.Test 0.1
+
+import '../../src/qml/MMS'
+
+Item {
+    id: root
+
+    width: units.gu(40)
+    height: units.gu(40)
+
+    PreviewerVideo {
+        id: previewerVideo
+        objectName: "previewerVideo"
+
+        property var application: QtObject {
+            property bool fullscreen: false
+        }
+
+        function getContentType(filePath) {
+            return ContentType.Videos
+        }
+
+        anchors.fill: parent
+
+        attachment: {
+            "contentType": "video/mp4",
+            "filePath": Qt.resolvedUrl("./data/sample.mp4")
+        }
+    }
+
+    UbuntuTestCase {
+        id: previewerVideoTestCase
+        name: 'peviewerVideoTestCase'
+
+        when: windowShown
+
+        function test_load_video() {
+            var videoPlayer = findChild(previewerVideo, "videoPlayer")
+            verify(videoPlayer != null)
+            tryCompare(videoPlayer, "visible", true)
+
+            var toolbar = findChild(previewerVideo, "toolbar")
+            verify(toolbar != null)
+            tryCompare(toolbar, "collapsed", true)
+        }
+
+        function test_toggle_toolbar() {
+            var videoPlayer = findChild(previewerVideo, "videoPlayer")
+            verify(videoPlayer != null)
+            tryCompare(videoPlayer, "visible", true)
+
+            var toolbar = findChild(previewerVideo, "toolbar")
+            verify(toolbar != null)
+            tryCompare(toolbar, "collapsed", true)
+ 
+            mouseClick(videoPlayer)
+            tryCompare(toolbar, "collapsed", false)
+
+            mouseClick(videoPlayer)
+            tryCompare(toolbar, "collapsed", true)
+        }
+    }
+}

=== added file 'tests/qml/tst_QmlTests.cpp'
--- tests/qml/tst_QmlTests.cpp	1970-01-01 00:00:00 +0000
+++ tests/qml/tst_QmlTests.cpp	2016-02-05 00:58:50 +0000
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * Authors:
+ *  Arthur Mello <arthur.mello@canonical.com>
+ *  Ugo Riboni <ugo.riboni@canonical.com>
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+// Qt
+#include <QtQuickTest/QtQuickTest>
+#include <QtQml/QtQml>
+
+// local
+#include "audiorecorder.h"
+#include "fileoperations.h"
+#include "stickers-history-model.h"
+
+class TestContext : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString testDir READ testDir CONSTANT)
+
+public:
+    explicit TestContext(QObject* parent=0)
+        : QObject(parent)
+    {
+        QDir dir(m_temporary.path());
+        dir.mkpath("stickers");
+    }
+
+    QString testDir() const
+    {
+        return m_temporary.path();
+    }
+
+    QTemporaryDir m_temporary;
+};
+
+static QObject* TestContext_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
+{
+    Q_UNUSED(engine);
+    Q_UNUSED(scriptEngine);
+    return new TestContext();
+}
+
+static QObject* FileOperations_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
+{
+    Q_UNUSED(engine);
+    Q_UNUSED(scriptEngine);
+    return new FileOperations();
+}
+
+static QObject* StickersHistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
+{
+    Q_UNUSED(engine);
+    Q_UNUSED(scriptEngine);
+    return new StickersHistoryModel();
+}
+
+int main(int argc, char** argv)
+{
+    const char* uri = "messagingapp.private";
+    qmlRegisterType<AudioRecorder>(uri, 0, 1, "AudioRecorder");
+    qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
+    qmlRegisterSingletonType<StickersHistoryModel>(uri, 0, 1, "StickersHistoryModel", StickersHistoryModel_singleton_factory);
+
+    const char* testUri = "messagingapptest.private";
+    qmlRegisterSingletonType<TestContext>(testUri, 0, 1, "TestContext", TestContext_singleton_factory);
+
+    return quick_test_main(argc, argv, "QmlTests", 0);
+}
+
+#include "tst_QmlTests.moc"

=== added file 'tests/qml/tst_StickersHistoryModel.qml'
--- tests/qml/tst_StickersHistoryModel.qml	1970-01-01 00:00:00 +0000
+++ tests/qml/tst_StickersHistoryModel.qml	2016-02-05 00:58:50 +0000
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2015 Canonical Ltd.
+ *
+ * This file is part of messaging-app.
+ *
+ * messaging-app 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; version 3.
+ *
+ * messaging-app 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/>.
+ */
+
+import QtQuick 2.2
+import QtTest 1.0
+import Ubuntu.Components 1.3
+import Ubuntu.Test 0.1
+import messagingapp.private 0.1
+import messagingapptest.private 0.1
+
+Item {
+    id: root
+    width: 1
+    height: 1
+
+    property var model: StickersHistoryModel
+
+    SignalSpy {
+       id: countSpy
+       target: model
+       signalName: "rowCountChanged"
+    }
+
+    SignalSpy {
+       id: rowsInsertedSpy
+       target: model
+       signalName: "rowsInserted"
+    }
+
+    SignalSpy {
+       id: rowsMovedSpy
+       target: model
+       signalName: "rowsMoved"
+    }
+
+    SignalSpy {
+       id: dataChangedSpy
+       target: model
+       signalName: "dataChanged"
+    }
+
+    SignalSpy {
+       id: rowsRemovedSpy
+       target: model
+       signalName: "rowsRemoved"
+    }
+
+    UbuntuTestCase {
+        id: test
+        name: 'stickersHistoryModelTestCase'
+        when: windowShown
+
+        function init() {
+            model.databasePath = ":memory:"
+            model.limit = 10
+        }
+
+        function cleanup() {
+            model.databasePath = ""
+            countSpy.clear()
+            rowsInsertedSpy.clear()
+            dataChangedSpy.clear()
+            rowsMovedSpy.clear()
+            rowsRemovedSpy.clear()
+        }
+
+        function test_initiallyEmpty() {
+            compare(model.count, 0)
+        }
+
+        function test_addingEmptyOrInvalidDoesNothing() {
+            model.add("")
+            compare(model.count, 0)
+            model.add(null)
+            compare(model.count, 0)
+            model.add(undefined)
+            compare(model.count, 0)
+        }
+
+        function test_add_new() {
+            model.add("foo")
+            compare(model.count, 1)
+            compare(countSpy.count, 1)
+            compare(rowsInsertedSpy.count, 1)
+            compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
+            compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
+
+            model.add("bar")
+            compare(model.count, 2)
+            compare(countSpy.count, 2)
+            compare(rowsInsertedSpy.count, 2)
+            compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
+            compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
+        }
+
+        function test_signals() {
+            model.limit = 2
+
+            model.add("a")
+            compare(model.count, 1)
+            compare(countSpy.count, 1)
+            compare(rowsInsertedSpy.count, 1)
+            compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
+            compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
+
+            model.add("a")
+            compare(dataChangedSpy.count, 1)
+
+            model.add("b")
+            compare(model.count, 2)
+            compare(countSpy.count, 2)
+            compare(rowsInsertedSpy.count, 2)
+            compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
+            compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
+
+            model.add("a")
+            compare(rowsMovedSpy.count, 1)
+            compare(rowsMovedSpy.signalArguments[0][1], 1) // from first
+            compare(rowsMovedSpy.signalArguments[0][2], 1) // from last
+            compare(rowsMovedSpy.signalArguments[0][4], 0) // to
+            compare(dataChangedSpy.count, 2)
+
+            model.add("c")
+            compare(model.count, 2)
+            compare(rowsRemovedSpy.count, 1)
+            compare(rowsRemovedSpy.signalArguments[0][1], 2) // from
+            compare(rowsRemovedSpy.signalArguments[0][2], 2) // to
+        }
+
+        function test_get_and_order() {
+            model.add("a")
+            // datetimes have only millisecond precision, and we want to prevent
+            // the two entries to have the same timestamp
+            wait(100)
+            model.add("b")
+
+            var a = model.get(1)
+            verify(a !== null)
+            compare(a.sticker, "a")
+
+            var b = model.get(0)
+            verify(b !== null)
+            compare(b.sticker, "b")
+
+            verify(b.mostRecentUse.toISOString() > a.mostRecentUse.toISOString())
+
+            wait(100)
+            model.add("a")
+
+            a = model.get(0)
+            verify(a !== null)
+            compare(a.sticker, "a")
+
+            b = model.get(1)
+            verify(b !== null)
+            compare(b.sticker, "b")
+
+            verify(a.mostRecentUse.toISOString() > b.mostRecentUse.toISOString())
+        }
+
+        function test_limit_change() {
+            model.add("d")
+            model.add("c")
+            model.add("b")
+            model.add("a")
+            model.limit = 2
+
+            compare(model.count, 2)
+            compare(model.get(0).sticker, "a")
+            compare(model.get(1).sticker, "b")
+        }
+    }
+}

