mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-22 18:16:43 +00:00
feat(BRIDGE-37): Remote notification support
This commit is contained in:
@ -1330,6 +1330,7 @@ void QMLBackend::connectGrpcEvents() {
|
||||
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
|
||||
connect(client, &GRPCClient::repairStarted, this, &QMLBackend::repairStarted);
|
||||
connect(client, &GRPCClient::allUsersLoaded, this, &QMLBackend::allUsersLoaded);
|
||||
connect(client, &GRPCClient::userNotificationReceived, this, &QMLBackend::processUserNotification);
|
||||
|
||||
// cache events
|
||||
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
|
||||
@ -1418,3 +1419,25 @@ void QMLBackend::triggerRepair() const {
|
||||
app().grpc().triggerRepair();
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] notification The user notification received from the event loop.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::processUserNotification(bridgepp::UserNotification const& notification) {
|
||||
this->userNotificationStack_.push(notification);
|
||||
trayIcon_->showUserNotification(notification.title, notification.subtitle);
|
||||
emit receivedUserNotification(notification);
|
||||
}
|
||||
|
||||
void QMLBackend::userNotificationDismissed() {
|
||||
if (!this->userNotificationStack_.size()) return;
|
||||
|
||||
// Remove the user notification from the top of the queue as it has been dismissed.
|
||||
this->userNotificationStack_.pop();
|
||||
if (!this->userNotificationStack_.size()) return;
|
||||
|
||||
// Display the user notification that is on top of the queue, if there is one.
|
||||
auto notification = this->userNotificationStack_.top();
|
||||
emit receivedUserNotification(notification);
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <stack>
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -174,6 +175,8 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
||||
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
||||
void usersChanged(UserList *users); ///<Signal for the change of the 'users' property.
|
||||
void dockIconVisibleChanged(bool value); ///<Signal for the change of the 'dockIconVisible' property.
|
||||
void receivedUserNotification(bridgepp::UserNotification const& notification); ///< Signal to display the userNotification modal
|
||||
|
||||
|
||||
public slots: // slot for signals received from QML -> To be forwarded to Bridge via RPC Client calls.
|
||||
void toggleAutostart(bool active); ///< Slot for the autostart toggle.
|
||||
@ -209,6 +212,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'.
|
||||
void userNotificationDismissed(); ///< Slot to pop the notification from the stack and display the rest.
|
||||
|
||||
public slots: // slots for functions that need to be processed locally.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
@ -224,6 +228,7 @@ public slots: // slot for signals received from gRPC that need transformation in
|
||||
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
||||
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
|
||||
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
|
||||
void processUserNotification(bridgepp::UserNotification const& notification); ///< Slot for the userNotificationReceived gRCP event.
|
||||
|
||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||
@ -310,6 +315,7 @@ private: // data members
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||
std::stack<bridgepp::UserNotification> userNotificationStack_; ///< The stack which holds all of the active notifications that the user needs to acknowledge.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
<file>qml/icons/systray-mono-update.png</file>
|
||||
<file>qml/icons/systray-mono-warn.png</file>
|
||||
<file>qml/icons/systray.svg</file>
|
||||
<file>qml/icons/ic-notification-bell.svg</file>
|
||||
<file alias="bridge.svg">../../../../dist/bridge.svg</file>
|
||||
<file alias="bridgeMacOS.svg">../../../../dist/bridgeMacOS.svg</file>
|
||||
<file>qml/KeychainSettings.qml</file>
|
||||
@ -78,6 +79,7 @@
|
||||
<file>qml/MainWindow.qml</file>
|
||||
<file>qml/NoAccountView.qml</file>
|
||||
<file>qml/NotificationDialog.qml</file>
|
||||
<file>qml/UserNotificationDialog.qml</file>
|
||||
<file>qml/NotificationPopups.qml</file>
|
||||
<file>qml/Notifications/Notification.qml</file>
|
||||
<file>qml/Notifications/NotificationFilter.qml</file>
|
||||
|
||||
@ -331,6 +331,15 @@ void TrayIcon::showErrorPopupNotification(QString const &title, QString const &m
|
||||
this->showMessage(title, message, notificationErrorIcon_);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// Used only by user notifications received from the event loop
|
||||
/// \param[in] title The title.
|
||||
/// \param[in] subtitle The subtitle.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::showUserNotification(QString const &title, QString const &subtitle) {
|
||||
this->showMessage(title, subtitle, QSystemTrayIcon::NoIcon);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] svgPath The path of the SVG file for the icon.
|
||||
|
||||
@ -42,6 +42,8 @@ public: // data members
|
||||
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
|
||||
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
|
||||
void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
|
||||
void showUserNotification(QString const& title, QString const &subtitle); ///< Display an OS pop up notification (without icon).
|
||||
|
||||
|
||||
signals:
|
||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID
|
||||
|
||||
@ -22,6 +22,7 @@ Dialog {
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
property var notification
|
||||
property bool isUserNotification: false
|
||||
|
||||
modal: true
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
@ -39,13 +40,13 @@ Dialog {
|
||||
return "";
|
||||
}
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
|
||||
@ -109,4 +109,8 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.repairBridge
|
||||
}
|
||||
UserNotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userNotification
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,8 @@ QtObject {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Danger
|
||||
Danger,
|
||||
UserNotification
|
||||
}
|
||||
|
||||
property list<Action> action
|
||||
@ -36,6 +37,9 @@ QtObject {
|
||||
readonly property var occurred: active ? new Date() : undefined
|
||||
property string title // title is used in dialogs only
|
||||
property int type
|
||||
property string subtitle
|
||||
property string username
|
||||
|
||||
|
||||
onActiveChanged: {
|
||||
dismissed = false;
|
||||
|
||||
@ -62,7 +62,7 @@ QtObject {
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge]
|
||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge, root.userNotification]
|
||||
property Notification alreadyLoggedIn: Notification {
|
||||
brief: qsTr("Already signed in")
|
||||
description: qsTr("This account is already signed in.")
|
||||
@ -1187,7 +1187,7 @@ QtObject {
|
||||
}
|
||||
target: root
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
function onRepairStarted() {
|
||||
root.repairBridge.active = false;
|
||||
@ -1200,6 +1200,35 @@ QtObject {
|
||||
|
||||
}
|
||||
|
||||
property Notification userNotification: Notification {
|
||||
brief: title
|
||||
group: Notifications.Group.Dialogs
|
||||
type: Notification.NotificationType.UserNotification
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg" // If it's not included QML complains
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Okay")
|
||||
onTriggered: {
|
||||
root.userNotification.active = false;
|
||||
Backend.userNotificationDismissed();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
function onReceivedUserNotification(notification) {
|
||||
const userPrimaryEmailOrUsername = Backend.users.primaryEmailOrUsername(notification.userID)
|
||||
root.userNotification.title = notification.title
|
||||
root.userNotification.subtitle = notification.subtitle
|
||||
root.userNotification.description = notification.body
|
||||
root.userNotification.username = userPrimaryEmailOrUsername
|
||||
root.userNotification.active = true
|
||||
}
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
|
||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||
signal askDeleteAccount(var user)
|
||||
signal askEnableBeta
|
||||
|
||||
@ -73,6 +73,11 @@ T.ApplicationWindow {
|
||||
if (obj.shouldShow === false) {
|
||||
continue;
|
||||
}
|
||||
// User notifications should have display priority
|
||||
if (obj.shouldShow && obj.isUserNotification) {
|
||||
topmost = obj;
|
||||
break;
|
||||
}
|
||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
property var notification
|
||||
property bool isUserNotification: true
|
||||
padding: 40
|
||||
|
||||
modal: true
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 16
|
||||
Layout.preferredHeight: 64
|
||||
Layout.preferredWidth: 64
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return "";
|
||||
}
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.UserNotification:
|
||||
return "/qml/icons/ic-notification-bell.svg"
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
sourceSize.width: 64
|
||||
visible: source != ""
|
||||
}
|
||||
// Title Label
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 4
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.title
|
||||
wrapMode: Text.WordWrap
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
// Username or primary email
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 24
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.username
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.notification.username.length > 0
|
||||
type: Label.LabelType.Caption
|
||||
}
|
||||
// Subtitle
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 24
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.subtitle
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.notification.subtitle.length > 0
|
||||
type: Label.LabelType.Lead
|
||||
color: root.colorScheme.text_weak
|
||||
}
|
||||
Label {
|
||||
Layout.bottomMargin: 24
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.description
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Backend.openExternalLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 40
|
||||
|
||||
Repeater {
|
||||
model: root.notification.action
|
||||
|
||||
delegate: Button {
|
||||
Layout.fillWidth: true
|
||||
action: modelData
|
||||
colorScheme: root.colorScheme
|
||||
loading: modelData.loading
|
||||
secondary: index > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
|
||||
<g clip-path="url(#a)">
|
||||
<circle cx="13.031" cy="5.288" r="3.166" fill="#2C83DC" transform="rotate(-30 13.031 5.288)"/>
|
||||
<path fill="url(#b)" d="M3.599 27.28A19.757 19.757 0 0 1 36.793 8.115L53.92 25.808a12.42 12.42 0 0 0 4.581 2.998c3.454 1.288 5.556 4.214 3.201 7.05-2.76 3.325-8.795 8.475-21.796 15.981S19.428 61.994 15.169 62.723c-3.634.621-5.117-2.662-4.506-6.298a12.422 12.422 0 0 0-.306-5.466L3.6 27.28Z"/>
|
||||
<path fill="url(#c)" d="M3.599 27.28A19.757 19.757 0 0 1 36.793 8.115L53.92 25.808a12.42 12.42 0 0 0 4.581 2.998c3.454 1.288 5.556 4.214 3.201 7.05-2.76 3.325-8.795 8.475-21.796 15.981S19.428 61.994 15.169 62.723c-3.634.621-5.117-2.662-4.506-6.298a12.422 12.422 0 0 0-.306-5.466L3.6 27.28Z"/>
|
||||
<ellipse cx="37.094" cy="46.965" fill="url(#d)" rx="26.875" ry="3.75" transform="rotate(-30 37.094 46.965)"/>
|
||||
<ellipse cx="37.094" cy="46.965" fill="url(#e)" rx="26.875" ry="3.75" transform="rotate(-30 37.094 46.965)"/>
|
||||
<path fill="#fff" fill-opacity=".2" d="m48.156 19.855-2.032-2.1c-6.302 1.79-12.908 4.57-19.41 8.324-7.591 4.383-14.103 9.553-19.19 14.959l.914 3.201c4.88-5.52 11.835-11.152 20.16-15.958 6.703-3.87 13.425-6.704 19.558-8.426Z"/>
|
||||
<circle cx="36.469" cy="45.883" r="2.5" fill="url(#f)" transform="rotate(-30 36.469 45.883)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="d" x1="49.16" x2="37.094" y1="30.878" y2="49.439" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#256097"/>
|
||||
<stop offset="1" stop-color="#2C83DC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" x1="49.68" x2="49.037" y1="46.478" y2="51.592" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#27ABF4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#27ABF4" stop-opacity=".5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="35.719" x2="36.469" y1="43.133" y2="48.383" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".345" stop-color="#B2EAFE" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#B2EAFE"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(131.347 12.11 6.294) scale(52.6374 54.7116)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B2EAFE"/>
|
||||
<stop offset="1" stop-color="#27ABF4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="rotate(60 -23.22 57.501) scale(21.25 76.0937)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset=".46" stop-color="#fff" stop-opacity=".4"/>
|
||||
<stop offset=".58" stop-color="#B2EAFE" stop-opacity=".5"/>
|
||||
</radialGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h64v64H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
Reference in New Issue
Block a user