Pull to refresh

Пишем клиент для Slack с оповещениями

Reading time 6 min
Views 18K
Приветствую, Хабравчане! Сегодня Slack выпустили свой клиент для Windows. Но еще совсем недавно такого клиента не было и необходимость получать нормальные уведомления была необходимостью. Slack предлагал использовать приложение Chrome. У данного подхода было два минуса:
  1. Отсутствие возможности настроить, сколько времени будет показываться уведомление
  2. Если уведомление пропало, то пользователь никак об этом не узнает.



К примеру, ты ушел налить себе кофе, а тут кто-то написал в чат. Возвращаешься на рабочее место и… тишина! Ничего не происходило. Ты работаешь дальше, а человек всё ждет и ждёт, пока кто-нибудь ему ответит. Непорядок! Skype вежливо уведомляет тебя всплывающим окошком и нагло сигнализирует в таскбаре о том, что тебе пришло сообщение. Быстрее прочти, а то твой таскбар так и будем мигать желтым светом. Даже если ты ушел на весь день.

Отмотаем время на 1 месяц назад. Идем на страницу myawesometeam.slack.com/apps и видим отсутствие нативного клиента для Windows и Linux, вместо этого там приложения для Chrome. Расстраиваемся. Запускаем приложение, понимаем всю печальность бытия.

Я начал искать решение проблемы. Первым нашелся SlackUI. Он построен на базе CEF (Chromium Embedded Framework). Я уже было почти обрадовался, запустил клиент и увидел всё то же самое, что и в приложении Chrome. Уведомления пропадают через 10 секунд, никаких уведомлений о том, что что-то было, пока ты ходил за кофе.

Что ж, я начал гуглить и наткнулся на то, что в Qt WebKit можно написать свой плагин, в том числе и для уведомлений. Нашелся проект QupZilla и его плагины для Qt WebKit (https://github.com/QupZilla/qtwebkit-plugins). Это было то, что нужно!

Этап 1. Делаем плагин уведомлений для Qt WebKit


В файле .pri нам нужно добавить заголовочные файлы для плагина и заголовочные файлы Qt, чтобы он мог подхватить наш плагин:
HEADERS += $$PWD/qtwebkitplugin.h \
               $$[QT_INSTALL_HEADERS]/QtWebKit/qwebkitplatformplugin.h

SOURCES += $$PWD/qtwebkitplugin.cpp

DEFINES *= QT_STATICPLUGIN

Код самого плагина:
qtwebkitplugin.h
#include "qwebkitplatformplugin.h"

class QtWebKitPlugin : public QObject, public QWebKitPlatformPlugin
{
    Q_OBJECT
    Q_INTERFACES(QWebKitPlatformPlugin)

#if QT_VERSION >= 0x050000
    Q_PLUGIN_METADATA(IID "org.qtwebkit.QtWebKit.QtWebKitPlugins")
#endif

public:
    explicit QtWebKitPlugin();

    bool supportsExtension(Extension ext) const;
    QObject* createExtension(Extension ext) const;
};



qtwebkitplugin.cpp
bool QtWebKitPlugin::supportsExtension(Extension ext) const
{
    return (ext == Notifications);
}

QObject* QtWebKitPlugin::createExtension(Extension ext) const
{
    switch (ext) {
    case Notifications:
        return new NotificationPresenter();

    default:
        return 0;
    }
}

#if QT_VERSION < 0x050000
Q_EXPORT_PLUGIN2(qtwebkitplugins, QtWebKitPlugin)
#endif

#if (QT_VERSION < 0x050000)
Q_IMPORT_PLUGIN(qtwebkitplugins)
#else
Q_IMPORT_PLUGIN(QtWebKitPlugin)
#endif



Пока что ничего сложного нет. NotificationPresenter — это некий класс, который будет отображать наши уведомления, пусть даже и консоль отладки:

notificationpresenter.h
#include "qwebkitplatformplugin.h"

class NotificationPresenter : public QWebNotificationPresenter
{

public:
    explicit NotificationPresenter();

    void showNotification(const QWebNotificationData* data);
};


notificationpresenter.cpp
NotificationPresenter::NotificationPresenter()
    : QWebNotificationPresenter()
{
}

void NotificationPresenter::showNotification(const QWebNotificationData* data)
{
    qDebug() << "--------------------------";
    qDebug() << "Title:";
    qDebug() << data->title();
    qDebug() << "Message:";
    qDebug() << data->message();
    qDebug() << "--------------------------";
}




Этап 2. Добавляем QWebView


Подключаем .pri файл к нашему проекту и добавляем в .pro файл зависимость от webkitwidgets:
...
QT += webkitwidgets
include(plugins/qtwebkit/qtwebkit-plugins.pri)
...


Добавляем на какую-нибудь форму QWebView, после чего нам нужно его немного настроить и подписаться на событие featurePermissionRequested:

Еще код?
void MainWindow::createWebView()
{
    webview->settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
    webview->settings()->setAttribute(QWebSettings::NotificationsEnabled, true);
    connect(webView->page(), SIGNAL(featurePermissionRequested(QWebFrame*,QWebPage::Feature)),
            this, SLOT(featureRequest(QWebFrame*,QWebPage::Feature)));
}

void MainWindow::featureRequest(QWebFrame *frame, QWebPage::Feature feature)
{
    qDebug() << frame->url();

    if (feature == QWebPage::Feature::Notifications)
    {
        int result = QMessageBox::question(this,
                              QString("Notification permission"),
                              QString("%1\nasks for notifications persmission. Should I allow?").arg(frame->url().toString()),
                              QMessageBox::StandardButton::Ok, QMessageBox::Cancel);

        if (result == QMessageBox::StandardButton::Ok)
        {
            webView->page()->setFeaturePermission(frame, feature,
                                                  QWebPage::PermissionPolicy::PermissionGrantedByUser);
        }
    }
}



Задаем тестовый url с уведомлением (например вот этот) и тестируем страницу. Должно показываться уведомление.

Этап 3. Добавляем куки и кэш на диск. Добавляем родные шрифты


В первую очередь бесят кривые шрифты. Поправим их сразу
webView->settings()->setFontFamily(QWebSettings::StandardFont, "Segoe UI");
webView->settings()->setFontSize(QWebSettings::DefaultFontSize, 16);


Если мы теперь перезапустим приложение, то нам снова нужно повторить процедуру подтверждения, заново вводить пароли и т.п. Значит, наступило время для того, чтобы сохранять куки и кэш на диске.
С кэшем чуть проще:
void MainWindow::setStoragePath()
{
    QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
    qDebug() << "Cache path" << path;

    storagePath = path;
    webView->page()->settings()->enablePersistentStorage(path);
}

С хранением куки посложнее. Я просто нашел готовое решение из примеров Qt. Можно легко найти в гугле по словам QWebView и CookieJar. Пример CookieJar можно найти в исходниках проекта
void MainWindow::setCookies()
{
    if (!cookieJar)
    {
        cookieJar = new CookieJar(this);
    }

    webView->page()->networkAccessManager()->setCookieJar(cookieJar);
}

После этого куки и кэш должны сохраняться и не нужно каждый раз вводить логины и пароли.

Этап 4. Подключаем дополнительные библиотеки


Для уведомлений я решил использовать Aseman Qt Tools.
Скачиваем, подключаем в .pro файл
include(asemantools/asemantools.pri)


Теперь в NotificationPresenter нашего плагина нужно протащить некий интерфейс для отображения уведомлений.
Добавляем в qtwebkit.pri
INCLUDEPATH += $$top_srcdir
HEADERS += $$top_srcdir/mainapplication.h


Здесь MainApplication — наследник от QApplication. Добавляем функции отображения уведомлений:

notificationpresenter.cpp
void NotificationPresenter::showNotification(const QWebNotificationData* data)
{
    mApp->showNotification(data->title(), data->message());
}



mainapplication.h
#include <QApplication>
#include "mainwindow.h"

#define mApp ((MainApplication*)MainApplication::instance())

class MainApplication : public QApplication
{
    Q_OBJECT

public:
    explicit MainApplication(int &argc, char** argv);

    void setMainWindow(MainWindow* window);
    MainWindow* getMainWindow();

    void showNotification(QString title, QString message);

    ~MainApplication();
private:
    MainWindow *m_window = 0;
};



mainapplication.cpp
MainWindow *MainApplication::getMainWindow()
{
    if (!m_window){
        m_window = new MainWindow();
    }

    return m_window;
}

void MainApplication::showNotification(QString title, QString message)
{
    getMainWindow()->showNotification(title, message);
}



Добавляем AsemanNotification в главное окно программы и заставляем таскбар мигать желтым светом:
MainWindow::MainWindow() {
    // ...
    notification = new AsemanNativeNotification(this);
    // ...
}

void MainWindow::showNotification(QString title, QString message)
{
    notification->sendNotify(title, message, "://images/png/Slack.png", 0, 100000); // Показываем уведомление
    QApplication::alert(this); // Мигаем таскбаром
}

Компилируем. Запускаем тестовую страницу. Должны появиться настоящие уведомления.

Этап 5. Пробуем собрать под линукс


И тут мы приехали. В Linux разные DE и все будет работать вверх тормашками.
В Unity иконка трея будет показываться в левом верхнем углу экрана. Выглядит это примерно так:

В Gnome 3 уведомления из AsemanTools у меня постоянно уползали куда-то за пределы экрана. Внятных решений я на нашел и снова за линукс стало грустно и обидно. Ничего не работает из коробки, нужно вечные пляски с бубном

Итоги


В результате получен опыт создания приложения на основе WebKit, а также создания плагинов для Qt.
Результат работы:


Ссылка на получившийся проект на Github

Настало время немного привести код в порядок, заварить кофе и вернуться к компьютеру, где радостно мигает желтый значок Slack в таскбаре. Запушить все изменения и ждать, когда радужный единорог проверит твой проект, чтобы ткнуть тебя головой в…
Tags:
Hubs:
+22
Comments 13
Comments Comments 13

Articles