diff --git a/CMakeLists.txt b/CMakeLists.txt
index 60b052893..7c8ff1853 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -57,6 +57,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(DESKTOP_FILE morph-browser.desktop)
+set(PUSH_HELPER_DIR "lib/ubuntu-push-client/legacy-helpers")
# uninstall target
configure_file(
@@ -94,4 +95,6 @@ add_subdirectory(doc)
install(FILES morph-browser.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/morph-browser)
install(FILES morph-browser-splash.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/morph-browser)
+install(PROGRAMS push-helper/morph-browser-helper.sh DESTINATION ${PUSH_HELPER_DIR} RENAME morph-browser)
+
add_subdirectory(click-hooks)
diff --git a/debian/morph-browser-apparmor.manifest b/debian/morph-browser-apparmor.manifest
index 20da2280f..2d90b2219 100644
--- a/debian/morph-browser-apparmor.manifest
+++ b/debian/morph-browser-apparmor.manifest
@@ -26,7 +26,7 @@
],
"template_variables": {
"APP_ID_DBUS": "morph_2dbrowser",
- "APP_PKGNAME_DBUS": "morph_2dbrowser",
+ "APP_PKGNAME_DBUS": "_",
"APP_PKGNAME": "morph-browser",
"CLICK_DIR": "/usr/share/morph-browser"
},
diff --git a/debian/morph-browser.install b/debian/morph-browser.install
index eae71a260..de61bddc8 100644
--- a/debian/morph-browser.install
+++ b/debian/morph-browser.install
@@ -1,4 +1,5 @@
usr/bin/morph-browser
+usr/lib/ubuntu-push-client/legacy-helpers
usr/share/morph-browser/*.qml
usr/share/morph-browser/qmldir
usr/share/morph-browser/*.js
diff --git a/push-helper/morph-browser-helper.sh b/push-helper/morph-browser-helper.sh
new file mode 100644
index 000000000..5ebbc4dfe
--- /dev/null
+++ b/push-helper/morph-browser-helper.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+cp $1 $2
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index d6f653425..a22366661 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -30,6 +30,8 @@ set(COMMONLIB_SRC
input-method-handler.cpp
meminfo.cpp
mime-database.cpp
+ notifications-proxy.cpp
+ pushclient/pushclient.cpp
session-storage.cpp
single-instance-manager.cpp
)
@@ -44,6 +46,7 @@ target_link_libraries(${COMMONLIB}
Qt5::Qml
Qt5::Quick
Qt5::Widgets
+ Qt5DBus
Qt5WebEngine
Qt5WebEngineCore
${LIBAPPARMOR_LDFLAGS}
diff --git a/src/app/DomainSettingsPage.qml b/src/app/DomainSettingsPage.qml
index a19c33c9e..b74b60a10 100644
--- a/src/app/DomainSettingsPage.qml
+++ b/src/app/DomainSettingsPage.qml
@@ -171,6 +171,7 @@ FocusScope {
readonly property string domain: model.domain
readonly property int userAgentId: model.userAgentId
readonly property int locationPreference: model.allowLocation
+ readonly property int notificationsPreference: model.allowNotifications
height: isCurrentItem ? layout.height : units.gu(5)
color: isCurrentItem ? ((theme.palette.selected.background.hslLightness > 0.5) ? Qt.darker(theme.palette.selected.background, 1.05) : Qt.lighter(theme.palette.selected.background, 1.5)) : theme.palette.normal.background
@@ -210,12 +211,11 @@ FocusScope {
CheckBox {
checked: model.allowCustomUrlSchemes
- onTriggered: DomainSettingsModel.allowCustomUrlSchemes(model.domain, checked)
+ onTriggered: DomainSettingsModel.allowCustomUrlSchemes(model.domain, checked, false)
anchors.verticalCenter: parent.verticalCenter
}
}
-
Row {
spacing: units.gu(1.5)
height: units.gu(1)
@@ -234,6 +234,25 @@ FocusScope {
anchors.verticalCenter: parent.verticalCenter
}
}
+
+ Row {
+ spacing: units.gu(1.5)
+ height: units.gu(1)
+ visible: item.ListView.isCurrentItem
+
+ Label {
+ width: parent.width * 0.5
+ text: i18n.tr("send notifications")
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ ComboBox {
+ model: [ i18n.tr("Ask each time"), i18n.tr("Allowed"), i18n.tr("Denied") ]
+ currentIndex: item.notificationsPreference
+ onCurrentIndexChanged: DomainSettingsModel.setNotificationsPreference(item.domain, currentIndex, false)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
Row {
spacing: units.gu(1.5)
diff --git a/src/app/NotificationsAccessDialog.qml b/src/app/NotificationsAccessDialog.qml
new file mode 100644
index 000000000..ac15fceff
--- /dev/null
+++ b/src/app/NotificationsAccessDialog.qml
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020 UBports Foundation
+ *
+ * This file is part of morph-browser.
+ *
+ * morph-browser 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.
+ *
+ * morph-browser 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 .
+ */
+
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+
+Dialog {
+ id: dialog
+
+ property string securityOrigin
+ property bool showRememberDecisionCheckBox
+ modal: true
+
+ title: i18n.tr("Permission Request")
+ text: securityOrigin + "
" + i18n.tr("This page wants to create notifications.")
+
+ signal allow()
+ signal allowPermanently()
+ signal reject()
+ signal rejectPermanently()
+
+ onAllow: { PopupUtils.close(dialog); }
+ onAllowPermanently: { PopupUtils.close(dialog); }
+ onReject: { PopupUtils.close(dialog); }
+ onRejectPermanently: { PopupUtils.close(dialog); }
+
+ ListItemLayout {
+ visible: showRememberDecisionCheckBox
+ title.text: i18n.tr("Remember decision")
+ CheckBox {
+ id: rememberDecisionCheckBox
+ }
+ }
+ Button {
+ objectName: "allow"
+ text: i18n.tr("Allow")
+ color: theme.palette.normal.positive
+ onClicked: rememberDecisionCheckBox.checked ? allowPermanently() : allow()
+ }
+ Button {
+ objectName: "deny"
+ text: i18n.tr("Deny")
+ onClicked: rememberDecisionCheckBox.checked ? rejectPermanently() : reject()
+ }
+}
diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml
index c49a47009..ff5aa9248 100644
--- a/src/app/WebViewImpl.qml
+++ b/src/app/WebViewImpl.qml
@@ -231,6 +231,44 @@ WebView {
mediaAccessDialog.origin = securityOrigin;
mediaAccessDialog.feature = feature;
break;
+
+ case WebEngineView.Notifications:
+
+ var domain = UrlUtils.extractHost(securityOrigin);
+ var notificationsPreference = DomainSettingsModel.getNotificationsPreference(domain);
+
+ if (notificationsPreference === DomainSettingsModel.AllowNotificationsAccess)
+ {
+ grantFeaturePermission(securityOrigin, feature, true);
+ return;
+ }
+
+ if (notificationsPreference === DomainSettingsModel.DenyNotificationsAccess)
+ {
+ grantFeaturePermission(securityOrigin, feature, false);
+ return;
+ }
+
+ var notificationsAccessDialog = PopupUtils.open(Qt.resolvedUrl("NotificationsAccessDialog.qml"), this);
+ notificationsAccessDialog.securityOrigin = securityOrigin;
+ notificationsAccessDialog.showRememberDecisionCheckBox = (domain !== "") && ! incognito
+ notificationsAccessDialog.allow.connect(function() {
+ grantFeaturePermission(securityOrigin, feature, true);
+ DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.AllowNotificationsAccess, true);
+ });
+ notificationsAccessDialog.allowPermanently.connect(function() {
+ grantFeaturePermission(securityOrigin, feature, true);
+ DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.AllowNotificationsAccess, false);
+ });
+ notificationsAccessDialog.reject.connect(function() {
+ grantFeaturePermission(securityOrigin, feature, false);
+ DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.DenyNotificationsAccess, true);
+ });
+ notificationsAccessDialog.rejectPermanently.connect(function() {
+ grantFeaturePermission(securityOrigin, feature, false);
+ DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.DenyNotificationsAccess, false);
+ });
+ break;
}
}
diff --git a/src/app/browserapplication.cpp b/src/app/browserapplication.cpp
index 0c44835e4..5faf827d7 100644
--- a/src/app/browserapplication.cpp
+++ b/src/app/browserapplication.cpp
@@ -45,6 +45,7 @@
#include "input-method-handler.h"
#include "meminfo.h"
#include "mime-database.h"
+#include "notifications-proxy.h"
#include "session-storage.h"
BrowserApplication::BrowserApplication(int& argc, char** argv)
@@ -107,6 +108,7 @@ MAKE_SINGLETON_FACTORY(DownloadsModel)
MAKE_SINGLETON_FACTORY(FileOperations)
MAKE_SINGLETON_FACTORY(MemInfo)
MAKE_SINGLETON_FACTORY(MimeDatabase)
+MAKE_SINGLETON_FACTORY(NotificationsProxy)
MAKE_SINGLETON_FACTORY(UserAgentsModel)
bool BrowserApplication::initialize(const QString& qmlFileSubPath
@@ -202,6 +204,7 @@ bool BrowserApplication::initialize(const QString& qmlFileSubPath
qmlRegisterSingletonType(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
qmlRegisterSingletonType(uri, 0, 1, "MemInfo", MemInfo_singleton_factory);
qmlRegisterSingletonType(uri, 0, 1, "MimeDatabase", MimeDatabase_singleton_factory);
+ qmlRegisterSingletonType(uri, 0, 1, "NotificationsProxy", NotificationsProxy_singleton_factory);
qmlRegisterType(uri, 0, 1, "SessionStorage");
qmlRegisterSingletonType(uri, 0, 1, "UserAgentsModel", UserAgentsModel_singleton_factory);
diff --git a/src/app/domain-settings-model.cpp b/src/app/domain-settings-model.cpp
index 95cd1cb33..f53b235fa 100644
--- a/src/app/domain-settings-model.cpp
+++ b/src/app/domain-settings-model.cpp
@@ -71,6 +71,7 @@ QHash DomainSettingsModel::roleNames() const
roles[DomainWithoutSubdomain] = "domainWithoutSubdomain";
roles[AllowCustomUrlSchemes] = "allowCustomUrlSchemes";
roles[AllowLocation] = "allowLocation";
+ roles[AllowNotifications] = "allowNotifications";
roles[UserAgentId] = "userAgentId";
roles[ZoomFactor] = "zoomFactor";
}
@@ -98,6 +99,8 @@ QVariant DomainSettingsModel::data(const QModelIndex& index, int role) const
return entry.allowCustomUrlSchemes;
case AllowLocation:
return entry.allowLocation;
+ case AllowNotifications:
+ return entry.allowNotifications;
case UserAgentId:
return entry.userAgentId;
case ZoomFactor:
@@ -111,16 +114,40 @@ void DomainSettingsModel::createOrAlterDatabaseSchema()
{
QSqlQuery createQuery(m_database);
QString query = QLatin1String("CREATE TABLE IF NOT EXISTS domainsettings "
- "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation INTEGER, "
+ "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation INTEGER, allowNotifications INTEGER, "
"userAgentId INTEGER, zoomFactor REAL, PRIMARY KEY(domain), FOREIGN KEY(userAgentId) REFERENCES useragents(id)); ");
createQuery.prepare(query);
createQuery.exec();
+
+ // Older version of the database schema didn’t have the column 'allowNotifications'
+ QSqlQuery tableInfoQuery(m_database);
+ query = QLatin1String("PRAGMA TABLE_INFO(domainsettings);");
+ tableInfoQuery.prepare(query);
+ tableInfoQuery.exec();
+
+ bool missingAllowNotificationsColumn = true;
+
+ while (tableInfoQuery.next()) {
+ if (tableInfoQuery.value("name").toString() == "allowNotifications") {
+ missingAllowNotificationsColumn = false;
+ }
+ if (!missingAllowNotificationsColumn) {
+ break;
+ }
+ }
+
+ if (missingAllowNotificationsColumn) {
+ QSqlQuery addFolderColumnQuery(m_database);
+ query = QLatin1String("ALTER TABLE domainsettings ADD COLUMN allowNotifications INTEGER;");
+ addFolderColumnQuery.prepare(query);
+ addFolderColumnQuery.exec();
+ }
}
void DomainSettingsModel::populateFromDatabase()
{
QSqlQuery populateQuery(m_database);
- QString query = QLatin1String("SELECT domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, userAgentId, zoomFactor "
+ QString query = QLatin1String("SELECT domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, allowNotifications, userAgentId, zoomFactor "
"FROM domainsettings;");
populateQuery.prepare(query);
populateQuery.exec();
@@ -131,6 +158,7 @@ void DomainSettingsModel::populateFromDatabase()
entry.domainWithoutSubdomain = populateQuery.value("domainWithoutSubdomain").toString();
entry.allowCustomUrlSchemes = populateQuery.value("allowCustomUrlSchemes").toBool();
entry.allowLocation = static_cast(populateQuery.value("allowLocation").toInt());
+ entry.allowNotifications = static_cast(populateQuery.value("allowNotifications").toInt());
entry.userAgentId = populateQuery.value("userAgentId").toInt();
entry.zoomFactor = populateQuery.value("zoomFactor").isNull() ? std::numeric_limits::quiet_NaN()
: populateQuery.value("zoomFactor").toDouble();
@@ -194,9 +222,9 @@ bool DomainSettingsModel::areCustomUrlSchemesAllowed(const QString& domain)
return m_entries[index].allowCustomUrlSchemes;
}
-void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allow)
+void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allow, bool incognito)
{
- insertEntry(domain);
+ insertEntry(domain, incognito);
int index = getIndexForDomain(domain);
if (index != -1) {
@@ -206,12 +234,15 @@ void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allo
}
entry.allowCustomUrlSchemes = allow;
Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << AllowCustomUrlSchemes);
- QSqlQuery query(m_database);
- static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowCustomUrlSchemes=? WHERE domain=?;");
- query.prepare(updateStatement);
- query.addBindValue(allow);
- query.addBindValue(domain);
- query.exec();
+ if (!incognito)
+ {
+ QSqlQuery query(m_database);
+ static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowCustomUrlSchemes=? WHERE domain=?;");
+ query.prepare(updateStatement);
+ query.addBindValue(allow);
+ query.addBindValue(domain);
+ query.exec();
+ }
}
}
@@ -228,7 +259,7 @@ DomainSettingsModel::AllowLocationPreference DomainSettingsModel::getLocationPre
void DomainSettingsModel::setLocationPreference(const QString& domain, DomainSettingsModel::AllowLocationPreference preference)
{
- insertEntry(domain);
+ insertEntry(domain, false);
int index = getIndexForDomain(domain);
if (index != -1) {
@@ -247,6 +278,42 @@ void DomainSettingsModel::setLocationPreference(const QString& domain, DomainSet
}
}
+DomainSettingsModel::NotificationsPreference DomainSettingsModel::getNotificationsPreference(const QString& domain) const
+{
+ int index = getIndexForDomain(domain);
+ if (index == -1)
+ {
+ return NotificationsPreference::AskForNotificationsAccess;
+ }
+
+ return m_entries[index].allowNotifications;
+}
+
+void DomainSettingsModel::setNotificationsPreference(const QString& domain, DomainSettingsModel::NotificationsPreference preference, bool incognito)
+{
+ insertEntry(domain, incognito);
+
+ int index = getIndexForDomain(domain);
+ if (index != -1) {
+ DomainSetting& entry = m_entries[index];
+ if (entry.allowNotifications == preference) {
+ return;
+ }
+ entry.allowNotifications = preference;
+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << AllowNotifications);
+
+ if (!incognito)
+ {
+ QSqlQuery query(m_database);
+ static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowNotifications=? WHERE domain=?;");
+ query.prepare(updateStatement);
+ query.addBindValue(entry.allowNotifications);
+ query.addBindValue(domain);
+ query.exec();
+ }
+ }
+}
+
int DomainSettingsModel::getUserAgentId(const QString& domain) const
{
int index = getIndexForDomain(domain);
@@ -260,7 +327,7 @@ int DomainSettingsModel::getUserAgentId(const QString& domain) const
void DomainSettingsModel::setUserAgentId(const QString& domain, int userAgentId)
{
- insertEntry(domain);
+ insertEntry(domain, false);
int index = getIndexForDomain(domain);
if (index != -1) {
@@ -314,7 +381,7 @@ double DomainSettingsModel::getZoomFactor(const QString& domain) const
void DomainSettingsModel::setZoomFactor(const QString& domain, double zoomFactor)
{
- insertEntry(domain);
+ insertEntry(domain, false);
int index = getIndexForDomain(domain);
if (index != -1) {
@@ -334,7 +401,7 @@ void DomainSettingsModel::setZoomFactor(const QString& domain, double zoomFactor
}
}
-void DomainSettingsModel::insertEntry(const QString &domain)
+void DomainSettingsModel::insertEntry(const QString &domain, bool incognito)
{
if (contains(domain))
{
@@ -347,23 +414,28 @@ void DomainSettingsModel::insertEntry(const QString &domain)
entry.domainWithoutSubdomain = DomainUtils::getDomainWithoutSubdomain(domain);
entry.allowCustomUrlSchemes = false;
entry.allowLocation = AllowLocationPreference::AskForLocationAccess;
+ entry.allowNotifications = NotificationsPreference::AskForNotificationsAccess;
entry.userAgentId = 0;
entry.zoomFactor = std::numeric_limits::quiet_NaN();
m_entries.append(entry);
endInsertRows();
Q_EMIT rowCountChanged();
- QSqlQuery query(m_database);
- static QString insertStatement = QLatin1String("INSERT INTO domainsettings (domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, userAgentId, zoomFactor)"
- " VALUES (?, ?, ?, ?, ?, ?);");
- query.prepare(insertStatement);
- query.addBindValue(entry.domain);
- query.addBindValue(entry.domainWithoutSubdomain);
- query.addBindValue(entry.allowCustomUrlSchemes);
- query.addBindValue(entry.allowLocation);
- query.addBindValue((entry.userAgentId > 0) ? entry.userAgentId : QVariant());
- query.addBindValue(entry.zoomFactor);
- query.exec();
+ if (!incognito)
+ {
+ QSqlQuery query(m_database);
+ static QString insertStatement = QLatin1String("INSERT INTO domainsettings (domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, allowNotifications, userAgentId, zoomFactor)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?);");
+ query.prepare(insertStatement);
+ query.addBindValue(entry.domain);
+ query.addBindValue(entry.domainWithoutSubdomain);
+ query.addBindValue(entry.allowCustomUrlSchemes);
+ query.addBindValue(entry.allowLocation);
+ query.addBindValue(entry.allowNotifications);
+ query.addBindValue((entry.userAgentId > 0) ? entry.userAgentId : QVariant());
+ query.addBindValue(entry.zoomFactor);
+ query.exec();
+ }
}
void DomainSettingsModel::removeEntry(const QString &domain)
@@ -390,10 +462,11 @@ void DomainSettingsModel::removeEntry(const QString &domain)
void DomainSettingsModel::removeObsoleteEntries()
{
QSqlQuery query(m_database);
- static QString deleteStatement = QLatin1String("DELETE FROM domainsettings WHERE allowCustomUrlSchemes=? AND allowLocation=? AND userAgentId IS NULL AND zoomFactor IS NULL;");
+ static QString deleteStatement = QLatin1String("DELETE FROM domainsettings WHERE allowCustomUrlSchemes=? AND allowLocation=? AND allowNotifications=? AND userAgentId IS NULL AND zoomFactor IS NULL;");
query.prepare(deleteStatement);
query.addBindValue(false);
- query.addBindValue(false);
+ query.addBindValue(AllowLocationPreference::AskForLocationAccess);
+ query.addBindValue(NotificationsPreference::AskForNotificationsAccess);
query.exec();
}
diff --git a/src/app/domain-settings-model.h b/src/app/domain-settings-model.h
index d3271584c..e84fc8019 100644
--- a/src/app/domain-settings-model.h
+++ b/src/app/domain-settings-model.h
@@ -33,6 +33,7 @@ class DomainSettingsModel : public QAbstractListModel
Q_PROPERTY(double defaultZoomFactor READ defaultZoomFactor WRITE setDefaultZoomFactor)
Q_ENUMS(AllowLocationPreference)
+ Q_ENUMS(NotificationsPreference)
Q_ENUMS(Roles)
public:
@@ -45,11 +46,18 @@ class DomainSettingsModel : public QAbstractListModel
DenyLocationAccess = 2
};
+ enum NotificationsPreference {
+ AskForNotificationsAccess = 0,
+ AllowNotificationsAccess = 1,
+ DenyNotificationsAccess = 2
+ };
+
enum Roles {
Domain = Qt::UserRole + 1,
DomainWithoutSubdomain,
AllowCustomUrlSchemes,
AllowLocation,
+ AllowNotifications,
UserAgentId,
ZoomFactor
};
@@ -68,15 +76,17 @@ class DomainSettingsModel : public QAbstractListModel
Q_INVOKABLE bool contains(const QString& domain) const;
Q_INVOKABLE void deleteAndResetDataBase();
Q_INVOKABLE bool areCustomUrlSchemesAllowed(const QString& domain);
- Q_INVOKABLE void allowCustomUrlSchemes(const QString& domain, bool allow);
+ Q_INVOKABLE void allowCustomUrlSchemes(const QString& domain, bool allow, bool incognito);
Q_INVOKABLE AllowLocationPreference getLocationPreference(const QString& domain) const;
Q_INVOKABLE void setLocationPreference(const QString& domain, AllowLocationPreference preference);
+ Q_INVOKABLE NotificationsPreference getNotificationsPreference(const QString& domain) const;
+ Q_INVOKABLE void setNotificationsPreference(const QString& domain, NotificationsPreference, bool incognito);
Q_INVOKABLE int getUserAgentId(const QString& domain) const;
Q_INVOKABLE void setUserAgentId(const QString& domain, int userAgentId);
Q_INVOKABLE void removeUserAgentIdFromAllDomains(int userAgentId);
Q_INVOKABLE double getZoomFactor(const QString& domain) const;
Q_INVOKABLE void setZoomFactor(const QString& domain, double zoomFactor);
- Q_INVOKABLE void insertEntry(const QString& domain);
+ Q_INVOKABLE void insertEntry(const QString& domain, bool incognito);
Q_INVOKABLE void removeEntry(const QString& domain);
Q_SIGNALS:
@@ -93,6 +103,7 @@ class DomainSettingsModel : public QAbstractListModel
QString domainWithoutSubdomain;
bool allowCustomUrlSchemes;
AllowLocationPreference allowLocation;
+ NotificationsPreference allowNotifications;
int userAgentId;
double zoomFactor;
};
diff --git a/src/app/notifications-proxy.cpp b/src/app/notifications-proxy.cpp
new file mode 100644
index 000000000..7c9340750
--- /dev/null
+++ b/src/app/notifications-proxy.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 UBports Foundation
+ *
+ * This file is part of morph-browser.
+ *
+ * morph-browser 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.
+ *
+ * morph-browser 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 .
+ */
+
+#include "notifications-proxy.h"
+#include "pushclient/pushclient.h"
+
+#include
+#include
+#include
+
+NotificationsProxy::NotificationsProxy(QObject* parent) : QObject(parent)
+{
+ m_appId = QString();
+ m_isWebApp = false;
+}
+
+void NotificationsProxy::setAppId(const QString &appId)
+{
+ m_appId = appId;
+ PushClient::instance()->setAppId(m_appId);
+ m_isWebApp = ! m_appId.startsWith("_");
+}
+
+void NotificationsProxy::sendNotification(QObject * notificationObject) const
+{
+ QWebEngineNotification * notification = qobject_cast(notificationObject);
+ QJsonObject message = buildMessage(notification->tag(), notification->origin(), notification->title(), notification->message());
+ // send notification, update it if tag does already exist
+ PushClient::instance()->update(notification->tag(), message);
+}
+
+void NotificationsProxy::updateCount() const
+{
+ PushClient::instance()->updateCount();
+}
+
+QJsonObject NotificationsProxy::buildMessage(const QString & tag, const QUrl & origin, const QString & title, const QString & body) const
+{
+ QJsonObject notification;
+ notification["tag"] = tag;
+ notification["card"] = buildCard(origin, title, body);
+ notification["sound"] = false;
+ QJsonObject message;
+ message["notification"] = notification;
+ return message;
+}
+
+QJsonObject NotificationsProxy::buildCard(const QUrl & origin, const QString & title, const QString & body) const
+{
+ QJsonObject card;
+ card["summary"] = title;
+ card["body"] = body;
+ card["popup"] = true;
+ card["persist"] = true;
+
+ QJsonArray actions = QJsonArray();
+ QString actionUri = m_isWebApp ? QString("appid://%1/%2/current-user-version").arg(m_appId.split("_").at(0), m_appId.split("_").at(1)) : origin.toString();
+ actions.append(actionUri);
+ card["actions"] = actions;
+ return card;
+}
diff --git a/src/app/notifications-proxy.h b/src/app/notifications-proxy.h
new file mode 100644
index 000000000..4819f2af1
--- /dev/null
+++ b/src/app/notifications-proxy.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 UBports Foundation
+ *
+ * This file is part of morph-browser.
+ *
+ * morph-browser 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.
+ *
+ * morph-browser 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 .
+ */
+
+#ifndef __NOTIFICATIONS_PROXY_H__
+#define __NOTIFICATIONS_PROXY_H__
+
+#include
+#include
+
+class NotificationsProxy : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit NotificationsProxy(QObject* parent=0);
+
+ Q_INVOKABLE void setAppId(const QString & appId);
+ Q_INVOKABLE void sendNotification(QObject * notificationObject) const;
+ Q_INVOKABLE void updateCount() const;
+
+private:
+ QJsonObject buildMessage(const QString & tag, const QUrl & origin, const QString & title, const QString & body) const;
+ QJsonObject buildCard(const QUrl & origin, const QString & title, const QString & body) const;
+
+ QString m_appId;
+ bool m_isWebApp;
+};
+
+#endif // __NOTIFICATIONS_PROXY_H__
diff --git a/src/app/pushclient/pushclient.cpp b/src/app/pushclient/pushclient.cpp
new file mode 100644
index 000000000..f60fd2ef7
--- /dev/null
+++ b/src/app/pushclient/pushclient.cpp
@@ -0,0 +1,189 @@
+/* Copyright (C) 2017 Dan Chapman (used pushclient.cpp of dekko as base)
+ * Copyright 2020 UBports Foundation
+ *
+ * This file is part of morph-browser
+ *
+ * morph-browser 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.
+ *
+ * morph-browser 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 .
+*/
+#include "pushclient.h"
+#include
+#include
+#include
+#include
+#include
+
+#define POSTAL_SERVICE "com.ubuntu.Postal"
+#define POSTAL_PATH "/com/ubuntu/Postal"
+#define POSTAL_IFACE "com.ubuntu.Postal"
+
+static QPointer s_client;
+PushClient *PushClient::instance()
+{
+ if (s_client.isNull()) {
+ s_client = new PushClient();
+ }
+ return s_client;
+}
+
+PushClient::PushClient(QObject *parent) : QObject(parent),
+ m_conn(QDBusConnection::sessionBus())
+{
+}
+
+void PushClient::setAppId(const QString & appId)
+{
+ m_appId = appId;
+ updateCount();
+}
+
+//shamelessly stolen from accounts-polld
+bool PushClient::send(const QJsonObject &message)
+{
+ QDBusMessage msg = QDBusMessage::createMethodCall(POSTAL_SERVICE,
+ makePath(),
+ POSTAL_IFACE,
+ "Post");
+ msg << m_appId;
+ QByteArray data = QJsonDocument(message).toJson(QJsonDocument::Compact);
+ msg << QString::fromUtf8(data);
+
+ qDebug() << "[POST] >> " << msg;
+
+ QDBusMessage reply = m_conn.call(msg);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qDebug() << "[POST ERROR] " << reply.errorMessage();
+ return false;
+ }
+ qDebug() << "[POST SUCCESS] >> Message posted.";
+ QJsonObject n = message.value("notification").toObject();
+ QString tag = n.value("tag").toString();
+ updateCount(tag);
+ return true;
+}
+
+bool PushClient::update(const QString &tag, const QJsonObject &message)
+{
+ if (hasTag(tag)) {
+ clearPersistent(tag);
+ }
+ return send(message);
+}
+
+bool PushClient::hasTag(const QString &tag)
+{
+ return m_tags.contains(tag);
+}
+
+bool PushClient::clearPersistent(const QString &tag)
+{
+ if (m_tags.contains(tag)) {
+ qDebug() << "[REMOVE] >> Removing message: " << tag;
+ QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE,
+ makePath(),
+ POSTAL_IFACE,
+ "ClearPersistent");
+ message << m_appId;
+ message << tag;
+
+ QDBusMessage reply = m_conn.call(message);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qDebug() << "[REMOVE ERROR] " << reply.errorMessage();
+ return false;
+ }
+ qDebug() << "[REMOVE SUCCESS] Notification removed";
+ return updateCount(tag, true);
+ }
+ return false;
+}
+
+bool PushClient::updateCount(const QString &tag, const bool remove)
+{
+ qDebug() << "[COUNT] >> Updating launcher count";
+ if (!tag.isEmpty()) {
+ if (!remove && !m_tags.contains(tag)) {
+ qDebug() << "[COUNT] >> Tag not yet in persistent list. adding it now: " << tag;
+ m_tags << tag;
+ }
+
+ if (remove && m_tags.contains(tag)) {
+ qDebug() << "[COUNT] >> Removing tag from persistent list: " << tag;
+ m_tags.removeAll(tag);
+ }
+ }
+ else {
+ m_tags = getPersistent();
+ }
+
+ bool visible = m_tags.count() != 0;
+ QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE,
+ makePath(),
+ POSTAL_IFACE,
+ "SetCounter");
+ message << m_appId << m_tags.count() << visible;
+ bool result = m_conn.send(message);
+ if (result) {
+ qDebug() << "[COUNT] >> Updated.";
+ }
+ return result;
+}
+
+//shamelessly stolen from accounts-polld
+QByteArray PushClient::makePath()
+{
+ QByteArray path(QByteArrayLiteral("/com/ubuntu/Postal/"));
+
+ QByteArray pkg = m_appId.split('_').first().toUtf8();
+
+ // legacy apps have _ as path
+ if (pkg.count() == 0) {
+ path += '_';
+ }
+
+ // path for click apps
+ for (int i = 0; i < pkg.count(); i++) {
+ char buffer[10];
+ char c = pkg[i];
+ switch (c) {
+ case '+':
+ case '.':
+ case '-':
+ case ':':
+ case '~':
+ case '_':
+ sprintf(buffer, "_%.2x", c);
+ path += buffer;
+ break;
+ default:
+ path += c;
+ }
+ }
+ qDebug() << "[PATH] >> " << path;
+ return path;
+}
+
+QStringList PushClient::getPersistent()
+{
+ QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE,
+ makePath(),
+ POSTAL_IFACE,
+ "ListPersistent");
+ message << m_appId;
+ QDBusMessage reply = m_conn.call(message);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qDebug() << reply.errorMessage();
+ return QStringList();
+ }
+ QStringList tags = reply.arguments()[0].toStringList();
+ qDebug() << "[TAGS] >> " << tags;
+ return tags;
+}
diff --git a/src/app/pushclient/pushclient.h b/src/app/pushclient/pushclient.h
new file mode 100644
index 000000000..d4d68b462
--- /dev/null
+++ b/src/app/pushclient/pushclient.h
@@ -0,0 +1,54 @@
+/* Copyright (C) 2017 Dan Chapman (used pushclient.h of Dekko as base)
+ * Copyright 2020 UBports Foundation
+ *
+ * This file is part of morph-browser
+ *
+ * morph-browser 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.
+ *
+ * morph-browser 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 .
+*/
+#ifndef PUSHCLIENT_H
+#define PUSHCLIENT_H
+
+#include
+#include
+#include
+
+class PushClient : public QObject
+{
+ Q_OBJECT
+public:
+ explicit PushClient(QObject *parent = 0);
+
+ static PushClient *instance();
+
+ // set the app id
+ void setAppId(const QString & appId);
+
+ // Send a notification
+ bool send(const QJsonObject &message);
+ // Update a notification
+ bool update(const QString &tag, const QJsonObject &message);
+
+ bool hasTag(const QString &tag);
+ bool clearPersistent(const QString &tag);
+ bool updateCount(const QString &tag = QString(), const bool remove = false);
+
+private:
+ QByteArray makePath();
+ QStringList getPersistent();
+
+ QDBusConnection m_conn;
+ QStringList m_tags;
+ QString m_appId;
+};
+
+#endif // PUSHCLIENT_H
diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml
index 7402b4424..81d6be797 100644
--- a/src/app/webbrowser/Browser.qml
+++ b/src/app/webbrowser/Browser.qml
@@ -63,6 +63,10 @@ Common.BrowserView {
browser.bindExistingTab(tab);
}
+ function findTabIndexWithUrl(url) {
+ return internal.findTabIndexWithUrl(url);
+ }
+
function moveTab(from, to) {
if (from === to
|| from < 0 || from >= count
@@ -185,7 +189,7 @@ Common.BrowserView {
// handle custom schemes
if (UrlUtils.hasCustomScheme(url))
{
- if (! internal.areCustomUrlSchemesAllowed(currentDomain))
+ if (! DomainSettingsModel.areCustomUrlSchemesAllowed(currentDomain))
{
request.action = WebEngineNavigationRequest.IgnoreRequest;
@@ -193,14 +197,14 @@ Common.BrowserView {
allowCustomSchemesDialog.url = url;
allowCustomSchemesDialog.domain = currentDomain;
allowCustomSchemesDialog.showAllowPermanentlyCheckBox = ! browser.incognito;
- allowCustomSchemesDialog.allow.connect(function() {internal.allowCustomUrlSchemes(currentDomain, false);
- internal.navigateToUrlAsync(url);
- }
- );
- allowCustomSchemesDialog.allowPermanently.connect(function() {internal.allowCustomUrlSchemes(currentDomain, true);
- internal.navigateToUrlAsync(url);
- }
- );
+ allowCustomSchemesDialog.allow.connect(function() {
+ DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, true);
+ internal.navigateToUrlAsync(url);
+ });
+ allowCustomSchemesDialog.allowPermanently.connect(function() {
+ DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, false);
+ internal.navigateToUrlAsync(url);
+ });
}
return;
}
@@ -1432,6 +1436,15 @@ Common.BrowserView {
}
}
+ function findTabIndexWithUrl(url) {
+ var i;
+ for (i = 0; i < tabsModel.count; i++) {
+ if (tabsModel.get(i).url.toString() === url.toString())
+ return i;
+ }
+ return -1;
+ }
+
function undoCloseTab() {
if (!incognito && closedTabHistory.length > 0) {
// For triggering property change on closedTabHistory
@@ -1531,35 +1544,6 @@ Common.BrowserView {
return false
}
- // domains the user has allowed custom protocols for this (incognito) session
- property var domainsWithCustomUrlSchemesAllowed: []
-
- function allowCustomUrlSchemes(domain, allowPermanently) {
- domainsWithCustomUrlSchemesAllowed.push(domain);
-
- if (allowPermanently)
- {
- DomainSettingsModel.allowCustomUrlSchemes(domain, true);
- }
- }
-
- function areCustomUrlSchemesAllowed(domain) {
-
- for (var i in domainsWithCustomUrlSchemesAllowed) {
- if (domain === domainsWithCustomUrlSchemesAllowed[i]) {
- return true;
- }
- }
-
- if (DomainSettingsModel.areCustomUrlSchemesAllowed(domain))
- {
- domainsWithCustomUrlSchemesAllowed.push(domain);
- return true;
- }
-
- return false;
- }
-
function historyGoBack() {
if (currentWebview && currentWebview.canGoBack) {
internal.resetFocus()
@@ -1970,6 +1954,11 @@ Common.BrowserView {
internal.closeTabsWithUrl(download.url);
}
}
+
+ onPresentNotification: {
+ NotificationsProxy.updateCount();
+ NotificationsProxy.sendNotification(notification);
+ }
}
Connections {
diff --git a/src/app/webbrowser/morph-browser.qml b/src/app/webbrowser/morph-browser.qml
index 119aae939..db500c91e 100644
--- a/src/app/webbrowser/morph-browser.qml
+++ b/src/app/webbrowser/morph-browser.qml
@@ -61,6 +61,12 @@ QtObject {
// create path for pages printed to PDF
FileOperations.mkpath(Qt.resolvedUrl(cacheLocation) + "/pdf_tmp");
+
+ // set appId of NotificationsProxy
+ NotificationsProxy.setAppId("_morph-browser");
+
+ // update notification count
+ NotificationsProxy.updateCount();
}
// Array of all windows, sorted chronologically (most recently active last)
@@ -79,17 +85,25 @@ QtObject {
function openUrls(urls, newWindow, incognito) {
var window = getLastActiveWindow(incognito)
if (!window || newWindow) {
- window = windowFactory.createObject(null, {"incognito": incognito})
+ window = windowFactory.createObject(null, {"incognito": incognito});
}
for (var i in urls) {
- window.addTab(urls[i]).load()
+ var tabIndexWithUrl = window.tabsModel.findTabIndexWithUrl(urls[i]);
+
+ if (tabIndexWithUrl >= 0) {
+ window.tabsModel.selectTab(tabIndexWithUrl);
+ }
+ else {
+ window.addTab(urls[i]).load();
+ window.tabsModel.currentIndex = window.tabsModel.count - 1;
+ }
}
if (window.tabsModel.count === 0) {
- window.addTab().load()
+ window.addTab().load();
+ window.tabsModel.currentIndex = window.tabsModel.count - 1;
}
- window.tabsModel.currentIndex = window.tabsModel.count - 1
- window.show()
- window.requestActivate()
+ window.show();
+ window.requestActivate();
}
property var windowFactory: Component {
@@ -115,55 +129,56 @@ QtObject {
onActiveChanged: {
if (active) {
- var index = allWindows.indexOf(this)
+ var index = allWindows.indexOf(this);
if (index > -1) {
- allWindows.push(allWindows.splice(index, 1)[0])
+ allWindows.push(allWindows.splice(index, 1)[0]);
}
+ NotificationsProxy.updateCount();
}
}
onClosing: {
if (allWindows.length == 1) {
if (tabsModel.count > 0) {
- session.save()
+ session.save();
} else {
- session.clear()
+ session.clear();
}
}
if (incognito && (allWindows.length > 1)) {
// If the last incognito window is being closed,
// prune incognito entries from the downloads model
- var incognitoWindows = 0
+ var incognitoWindows = 0;
for (var w in allWindows) {
- var window = allWindows[w]
+ var window = allWindows[w];
if ((window !== this) && window.incognito) {
- ++incognitoWindows
+ ++incognitoWindows;
}
}
- if (incognitoWindows == 0) {
- DownloadsModel.pruneIncognitoDownloads()
+ if (incognitoWindows === 0) {
+ DownloadsModel.pruneIncognitoDownloads();
}
}
-
-
if (allWindows.length > 1)
{
for (var win in allWindows) {
if (this === allWindows[win]) {
- var tabs = allWindows[win].tabsModel
+ var tabs = allWindows[win].tabsModel;
for (var t = tabs.count - 1; t >= 0; --t) {
- //console.log("remove tab with url " + tabs.get(t).url)
- tabs.removeTab(t)
+ //console.log("remove tab with url " + tabs.get(t).url);
+ tabs.removeTab(t);
}
- allWindows.splice(win, 1)
- return
+ allWindows.splice(win, 1);
+ return;
}
}
}
- destroy()
+ NotificationsProxy.updateCount();
+
+ destroy();
}
Shortcut {
diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml
index 3235af014..bc4b25c56 100644
--- a/src/app/webcontainer/WebApp.qml
+++ b/src/app/webcontainer/WebApp.qml
@@ -512,6 +512,11 @@ Common.BrowserView {
console.log("a download was finished with path %1.".arg(download.path))
webapp.setDownloadComplete(download)
}
+
+ onPresentNotification: {
+ NotificationsProxy.updateCount();
+ NotificationsProxy.sendNotification(notification);
+ }
}
Connections {
diff --git a/src/app/webcontainer/WebViewImplOxide.qml b/src/app/webcontainer/WebViewImplOxide.qml
index ba96ef33e..f17aa4c3b 100644
--- a/src/app/webcontainer/WebViewImplOxide.qml
+++ b/src/app/webcontainer/WebViewImplOxide.qml
@@ -215,35 +215,6 @@ WebappWebview {
currentWebview.runJavaScript("window.location.href = '%1';".arg(targetUrl));
}
- // domains the user has allowed custom protocols for this (incognito) session
- property var domainsWithCustomUrlSchemesAllowed: []
-
- function allowCustomUrlSchemes(domain, allowPermanently) {
- domainsWithCustomUrlSchemesAllowed.push(domain);
-
- if (allowPermanently)
- {
- DomainSettingsModel.allowCustomUrlSchemes(domain, true);
- }
- }
-
- function areCustomUrlSchemesAllowed(domain) {
-
- for (var i in domainsWithCustomUrlSchemesAllowed) {
- if (domain === domainsWithCustomUrlSchemesAllowed[i]) {
- return true;
- }
- }
-
- if (DomainSettingsModel.areCustomUrlSchemesAllowed(domain))
- {
- domainsWithCustomUrlSchemesAllowed.push(domain);
- return true;
- }
-
- return false;
- }
-
function navigationRequestedDelegate(request) {
var url = request.url.toString();
@@ -255,7 +226,7 @@ WebappWebview {
// handle custom schemes
if (UrlUtils.hasCustomScheme(url))
{
- if (! areCustomUrlSchemesAllowed(currentDomain))
+ if (! DomainSettingsModel.areCustomUrlSchemesAllowed(currentDomain))
{
request.action = WebEngineNavigationRequest.IgnoreRequest;
@@ -263,14 +234,14 @@ WebappWebview {
allowCustomSchemesDialog.url = url;
allowCustomSchemesDialog.domain = currentDomain;
allowCustomSchemesDialog.showAllowPermanentlyCheckBox = true;
- allowCustomSchemesDialog.allow.connect(function() {allowCustomUrlSchemes(currentDomain, false);
- navigateToUrlAsync(url);
- }
- );
- allowCustomSchemesDialog.allowPermanently.connect(function() {allowCustomUrlSchemes(currentDomain, true);
- navigateToUrlAsync(url);
- }
- );
+ allowCustomSchemesDialog.allow.connect(function() {
+ DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, true);
+ navigateToUrlAsync(url);
+ });
+ allowCustomSchemesDialog.allowPermanently.connect(function() {
+ DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, false);
+ navigateToUrlAsync(url);
+ });
}
return;
}
diff --git a/src/app/webcontainer/webapp-container.qml b/src/app/webcontainer/webapp-container.qml
index 949a3c815..4e55b579a 100644
--- a/src/app/webcontainer/webapp-container.qml
+++ b/src/app/webcontainer/webapp-container.qml
@@ -63,6 +63,16 @@ BrowserWindow {
// Used for testing
signal schemeUriHandleFilterResult(string uri)
+ onActiveChanged: {
+ if (active) {
+ NotificationsProxy.updateCount();
+ }
+ }
+
+ onClosing: {
+ NotificationsProxy.updateCount();
+ }
+
function getWindowTitle() {
var webappViewTitle =
webappViewLoader.item
@@ -187,6 +197,12 @@ BrowserWindow {
// create path for pages printed to PDF
FileOperations.mkpath(Qt.resolvedUrl(cacheLocation) + "/pdf_tmp");
+
+ // set appId for NotificationsProxy
+ NotificationsProxy.setAppId(unversionedAppId);
+
+ // update notifications count
+ NotificationsProxy.updateCount();
}
function loadCustomUserScripts() {