diff --git a/resources.qrc b/resources.qrc
index c78b166fa..661550384 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -29,5 +29,6 @@
src/gui/tray/ActivityItemActions.qml
src/gui/tray/ActivityItemContent.qml
src/gui/tray/TalkReplyTextField.qml
+ src/gui/tray/CallNotificationDialog.qml
diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp
index d132e3ca4..b03282c75 100644
--- a/src/gui/generalsettings.cpp
+++ b/src/gui/generalsettings.cpp
@@ -146,6 +146,10 @@ GeneralSettings::GeneralSettings(QWidget *parent)
this, &GeneralSettings::slotToggleOptionalServerNotifications);
_ui->serverNotificationsCheckBox->setToolTip(tr("Server notifications that require attention."));
+ connect(_ui->callNotificationsCheckBox, &QAbstractButton::toggled,
+ this, &GeneralSettings::slotToggleCallNotifications);
+ _ui->callNotificationsCheckBox->setToolTip(tr("Show call notification dialogs."));
+
connect(_ui->showInExplorerNavigationPaneCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotShowInExplorerNavigationPane);
// Rename 'Explorer' appropriately on non-Windows
@@ -247,6 +251,8 @@ void GeneralSettings::loadMiscSettings()
ConfigFile cfgFile;
_ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons());
_ui->serverNotificationsCheckBox->setChecked(cfgFile.optionalServerNotifications());
+ _ui->callNotificationsCheckBox->setEnabled(_ui->serverNotificationsCheckBox->isEnabled());
+ _ui->callNotificationsCheckBox->setChecked(cfgFile.showCallNotifications());
_ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane());
_ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter());
auto newFolderLimit = cfgFile.newBigFolderSizeLimit();
@@ -428,6 +434,13 @@ void GeneralSettings::slotToggleOptionalServerNotifications(bool enable)
{
ConfigFile cfgFile;
cfgFile.setOptionalServerNotifications(enable);
+ _ui->callNotificationsCheckBox->setEnabled(enable);
+}
+
+void GeneralSettings::slotToggleCallNotifications(bool enable)
+{
+ ConfigFile cfgFile;
+ cfgFile.setShowCallNotifications(enable);
}
void GeneralSettings::slotShowInExplorerNavigationPane(bool checked)
diff --git a/src/gui/generalsettings.h b/src/gui/generalsettings.h
index 6c7134817..acc8793a9 100644
--- a/src/gui/generalsettings.h
+++ b/src/gui/generalsettings.h
@@ -48,6 +48,7 @@ private slots:
void saveMiscSettings();
void slotToggleLaunchOnStartup(bool);
void slotToggleOptionalServerNotifications(bool);
+ void slotToggleCallNotifications(bool);
void slotShowInExplorerNavigationPane(bool);
void slotIgnoreFilesEditor();
void slotCreateDebugArchive();
diff --git a/src/gui/generalsettings.ui b/src/gui/generalsettings.ui
index 3cc242864..27a71b8a3 100644
--- a/src/gui/generalsettings.ui
+++ b/src/gui/generalsettings.ui
@@ -6,8 +6,8 @@
0
0
- 554
- 558
+ 556
+ 563
@@ -90,6 +90,13 @@
+ -
+
+
+ Show Call Notifications
+
+
+
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index 9e4831abe..195ae3aca 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -159,6 +160,44 @@ void Systray::create()
}
}
+void Systray::createCallDialog(const Activity &callNotification)
+{
+ qCDebug(lcSystray) << "Starting a new call dialog for notification with id: " << callNotification._id << "with text: " << callNotification._subject;
+
+ if (_trayEngine && !_callsAlreadyNotified.contains(callNotification._id)) {
+ const QVariantMap talkNotificationData{
+ {"conversationToken", callNotification._talkNotificationData.conversationToken},
+ {"messageId", callNotification._talkNotificationData.messageId},
+ {"messageSent", callNotification._talkNotificationData.messageSent},
+ {"userAvatar", callNotification._talkNotificationData.userAvatar},
+ };
+
+ QVariantList links;
+ for(const auto &link : callNotification._links) {
+ links.append(QVariantMap{
+ {"imageSource", link._imageSource},
+ {"imageSourceHovered", link._imageSourceHovered},
+ {"label", link._label},
+ {"link", link._link},
+ {"primary", link._primary},
+ {"verb", link._verb},
+ });
+ }
+
+ const QVariantMap initialProperties{
+ {"talkNotificationData", talkNotificationData},
+ {"links", links},
+ {"subject", callNotification._subject},
+ {"link", callNotification._link},
+ };
+
+ const auto callDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/tray/CallNotificationDialog.qml"));
+ callDialog->createWithInitialProperties(initialProperties);
+
+ _callsAlreadyNotified.insert(callNotification._id);
+ }
+}
+
void Systray::slotNewUserSelected()
{
if (_trayEngine) {
@@ -308,6 +347,30 @@ void Systray::forceWindowInit(QQuickWindow *window) const
#endif
}
+void Systray::positionNotificationWindow(QQuickWindow *window) const
+{
+ if (!useNormalWindow()) {
+ window->setScreen(currentScreen());
+ if(geometry().isValid()) {
+ // On OSes where the QSystemTrayIcon geometry method isn't borked, we can actually figure out where the system tray is located
+ // We can therefore use our normal routines
+ const auto position = computeNotificationPosition(window->width(), window->height());
+ window->setPosition(position);
+ } else if (QProcessEnvironment::systemEnvironment().contains(QStringLiteral("XDG_CURRENT_DESKTOP")) &&
+ (QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")).contains(QStringLiteral("GNOME")))) {
+ // We can safely hardcode the top-right position for the notification when running GNOME
+ const auto position = computeNotificationPosition(window->width(), window->height(), 0, NotificationPosition::TopRight);
+ window->setPosition(position);
+ } else {
+ // For other DEs we play it safe and place the notification in the centre of the screen
+ const QPoint windowAdjustment(window->geometry().width() / 2, window->geometry().height() / 2);
+ const auto position = currentScreen()->geometry().center();// - windowAdjustment;
+ window->setPosition(position);
+ }
+ // TODO: Get actual notification positions for the DEs
+ }
+}
+
QScreen *Systray::currentScreen() const
{
const auto screen = QGuiApplication::screenAt(QCursor::pos());
@@ -446,8 +509,85 @@ QPoint Systray::computeWindowReferencePoint() const
Q_UNREACHABLE();
}
+QPoint Systray::computeNotificationReferencePoint(int spacing, NotificationPosition position) const
+{
+ auto trayIconCenter = calcTrayIconCenter();
+ auto taskbarScreenEdge = taskbarOrientation();
+ auto taskbarRect = taskbarGeometry();
+ const auto screenRect = currentScreenRect();
+
+ if(position == NotificationPosition::TopLeft) {
+ taskbarScreenEdge = TaskBarPosition::Top;
+ trayIconCenter = QPoint(0, 0);
+ taskbarRect = QRect(0, 0, screenRect.width(), 32);
+ } else if(position == NotificationPosition::TopRight) {
+ taskbarScreenEdge = TaskBarPosition::Top;
+ trayIconCenter = QPoint(screenRect.width(), 0);
+ taskbarRect = QRect(0, 0, screenRect.width(), 32);
+ } else if(position == NotificationPosition::BottomLeft) {
+ taskbarScreenEdge = TaskBarPosition::Bottom;
+ trayIconCenter = QPoint(0, screenRect.height());
+ taskbarRect = QRect(0, 0, screenRect.width(), 32);
+ } else if(position == NotificationPosition::BottomRight) {
+ taskbarScreenEdge = TaskBarPosition::Bottom;
+ trayIconCenter = QPoint(screenRect.width(), screenRect.height());
+ taskbarRect = QRect(0, 0, screenRect.width(), 32);
+ }
+
+ qCDebug(lcSystray) << "screenRect:" << screenRect;
+ qCDebug(lcSystray) << "taskbarRect:" << taskbarRect;
+ qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
+ qCDebug(lcSystray) << "trayIconCenter:" << trayIconCenter;
+
+ switch(taskbarScreenEdge) {
+ case TaskBarPosition::Bottom:
+ return {
+ trayIconCenter.x() < screenRect.center().x() ? screenRect.left() + spacing : screenRect.right() - spacing,
+ screenRect.bottom() - taskbarRect.height() - spacing
+ };
+ case TaskBarPosition::Left:
+ return {
+ screenRect.left() + taskbarRect.width() + spacing,
+ trayIconCenter.y() < screenRect.center().y() ? screenRect.top() + spacing : screenRect.bottom() - spacing
+ };
+ case TaskBarPosition::Top:
+ return {
+ trayIconCenter.x() < screenRect.center().x() ? screenRect.left() + spacing : screenRect.right() - spacing,
+ screenRect.top() + taskbarRect.height() + spacing
+ };
+ case TaskBarPosition::Right:
+ return {
+ screenRect.right() - taskbarRect.width() - spacing,
+ trayIconCenter.y() < screenRect.center().y() ? screenRect.top() + spacing : screenRect.bottom() - spacing
+ };
+ }
+ Q_UNREACHABLE();
+}
+
+QRect Systray::computeWindowRect(int spacing, const QPoint &topLeft, const QPoint &bottomRight) const
+{
+ const auto screenRect = currentScreenRect();
+ const auto rect = QRect(topLeft, bottomRight);
+ auto offset = QPoint();
+
+ if (rect.left() < screenRect.left()) {
+ offset.setX(screenRect.left() - rect.left() + spacing);
+ } else if (rect.right() > screenRect.right()) {
+ offset.setX(screenRect.right() - rect.right() - spacing);
+ }
+
+ if (rect.top() < screenRect.top()) {
+ offset.setY(screenRect.top() - rect.top() + spacing);
+ } else if (rect.bottom() > screenRect.bottom()) {
+ offset.setY(screenRect.bottom() - rect.bottom() - spacing);
+ }
+
+ return rect.translated(offset);
+}
+
QPoint Systray::computeWindowPosition(int width, int height) const
{
+ constexpr auto spacing = 4;
const auto referencePoint = computeWindowReferencePoint();
const auto taskbarScreenEdge = taskbarOrientation();
@@ -467,24 +607,7 @@ QPoint Systray::computeWindowPosition(int width, int height) const
Q_UNREACHABLE();
}();
const auto bottomRight = topLeft + QPoint(width, height);
- const auto windowRect = [=]() {
- const auto rect = QRect(topLeft, bottomRight);
- auto offset = QPoint();
-
- if (rect.left() < screenRect.left()) {
- offset.setX(screenRect.left() - rect.left() + 4);
- } else if (rect.right() > screenRect.right()) {
- offset.setX(screenRect.right() - rect.right() - 4);
- }
-
- if (rect.top() < screenRect.top()) {
- offset.setY(screenRect.top() - rect.top() + 4);
- } else if (rect.bottom() > screenRect.bottom()) {
- offset.setY(screenRect.bottom() - rect.bottom() - 4);
- }
-
- return rect.translated(offset);
- }();
+ const auto windowRect = computeWindowRect(spacing, topLeft, bottomRight);
qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
qCDebug(lcSystray) << "screenRect:" << screenRect;
@@ -494,17 +617,64 @@ QPoint Systray::computeWindowPosition(int width, int height) const
return windowRect.topLeft();
}
+QPoint Systray::computeNotificationPosition(int width, int height, int spacing, NotificationPosition position) const
+{
+ const auto referencePoint = computeNotificationReferencePoint(spacing, position);
+
+ auto trayIconCenter = calcTrayIconCenter();
+ auto taskbarScreenEdge = taskbarOrientation();
+ const auto screenRect = currentScreenRect();
+
+ if(position == NotificationPosition::TopLeft) {
+ taskbarScreenEdge = TaskBarPosition::Top;
+ trayIconCenter = QPoint(0, 0);
+ } else if(position == NotificationPosition::TopRight) {
+ taskbarScreenEdge = TaskBarPosition::Top;
+ trayIconCenter = QPoint(screenRect.width(), 0);
+ } else if(position == NotificationPosition::BottomLeft) {
+ taskbarScreenEdge = TaskBarPosition::Bottom;
+ trayIconCenter = QPoint(0, screenRect.height());
+ } else if(position == NotificationPosition::BottomRight) {
+ taskbarScreenEdge = TaskBarPosition::Bottom;
+ trayIconCenter = QPoint(screenRect.width(), screenRect.height());
+ }
+
+ const auto topLeft = [=]() {
+ switch(taskbarScreenEdge) {
+ case TaskBarPosition::Bottom:
+ return trayIconCenter.x() < screenRect.center().x() ? referencePoint - QPoint(0, height) : referencePoint - QPoint(width, height);
+ case TaskBarPosition::Left:
+ return trayIconCenter.y() < screenRect.center().y() ? referencePoint : referencePoint - QPoint(0, height);
+ case TaskBarPosition::Top:
+ return trayIconCenter.x() < screenRect.center().x() ? referencePoint : referencePoint - QPoint(width, 0);
+ case TaskBarPosition::Right:
+ return trayIconCenter.y() < screenRect.center().y() ? referencePoint - QPoint(width, 0) : QPoint(width, height);
+ }
+ Q_UNREACHABLE();
+ }();
+ const auto bottomRight = topLeft + QPoint(width, height);
+ const auto windowRect = computeWindowRect(spacing, topLeft, bottomRight);
+
+ qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
+ qCDebug(lcSystray) << "screenRect:" << screenRect;
+ qCDebug(lcSystray) << "windowRect (reference)" << QRect(topLeft, bottomRight);
+ qCDebug(lcSystray) << "windowRect (adjusted)" << windowRect;
+ qCDebug(lcSystray) << "referencePoint" << referencePoint;
+
+ return windowRect.topLeft();
+}
+
QPoint Systray::calcTrayIconCenter() const
{
- // QSystemTrayIcon::geometry() is broken for ages on most Linux DEs (invalid geometry returned)
- // thus we can use this only for Windows and macOS
-#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
- auto trayIconCenter = geometry().center();
- return trayIconCenter;
-#else
+ if(geometry().isValid()) {
+ // QSystemTrayIcon::geometry() is broken for ages on most Linux DEs (invalid geometry returned)
+ // thus we can use this only for Windows and macOS
+ auto trayIconCenter = geometry().center();
+ return trayIconCenter;
+ }
+
// On Linux, fall back to mouse position (assuming tray icon is activated by mouse click)
return QCursor::pos(currentScreen());
-#endif
}
AccessManagerFactory::AccessManagerFactory()
diff --git a/src/gui/systray.h b/src/gui/systray.h
index 0b4aabc3e..7941978a3 100644
--- a/src/gui/systray.h
+++ b/src/gui/systray.h
@@ -64,6 +64,9 @@ public:
enum class TaskBarPosition { Bottom, Left, Top, Right };
Q_ENUM(TaskBarPosition);
+
+ enum class NotificationPosition { Default, TopLeft, TopRight, BottomLeft, BottomRight };
+ Q_ENUM(NotificationPosition);
void setTrayEngine(QQmlApplicationEngine *trayEngine);
void create();
@@ -72,6 +75,7 @@ public:
bool isOpen();
QString windowTitle() const;
bool useNormalWindow() const;
+ void createCallDialog(const Activity &callNotification);
Q_INVOKABLE void pauseResumeSync();
Q_INVOKABLE bool syncIsPaused();
@@ -79,6 +83,7 @@ public:
Q_INVOKABLE void setClosed();
Q_INVOKABLE void positionWindow(QQuickWindow *window) const;
Q_INVOKABLE void forceWindowInit(QQuickWindow *window) const;
+ Q_INVOKABLE void positionNotificationWindow(QQuickWindow *window) const;
signals:
void currentUserChanged();
@@ -110,16 +115,21 @@ private:
QScreen *currentScreen() const;
QRect currentScreenRect() const;
QPoint computeWindowReferencePoint() const;
+ QPoint computeNotificationReferencePoint(int spacing = 20, NotificationPosition position = NotificationPosition::Default) const;
QPoint calcTrayIconCenter() const;
TaskBarPosition taskbarOrientation() const;
QRect taskbarGeometry() const;
+ QRect computeWindowRect(int spacing, const QPoint &topLeft, const QPoint &bottomRight) const;
QPoint computeWindowPosition(int width, int height) const;
+ QPoint computeNotificationPosition(int width, int height, int spacing = 20, NotificationPosition position = NotificationPosition::Default) const;
bool _isOpen = false;
bool _syncIsPaused = true;
QPointer _trayEngine;
AccessManagerFactory _accessManagerFactory;
+
+ QSet _callsAlreadyNotified;
};
} // namespace OCC
diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml
index 762598539..441505782 100644
--- a/src/gui/tray/ActivityItem.qml
+++ b/src/gui/tray/ActivityItem.qml
@@ -15,6 +15,7 @@ MouseArea {
readonly property bool isChatActivity: model.objectType === "chat" || model.objectType === "room" || model.objectType === "call"
readonly property bool isTalkReplyPossible: model.conversationToken !== ""
property bool isTalkReplyOptionVisible: model.messageSent !== ""
+ readonly property bool isCallActivity: model.objectType === "call"
signal fileActivityButtonClicked(string absolutePath)
diff --git a/src/gui/tray/CallNotificationDialog.qml b/src/gui/tray/CallNotificationDialog.qml
new file mode 100644
index 000000000..535cf6745
--- /dev/null
+++ b/src/gui/tray/CallNotificationDialog.qml
@@ -0,0 +1,244 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import Style 1.0
+import com.nextcloud.desktopclient 1.0
+import QtQuick.Layouts 1.2
+import QtMultimedia 5.15
+import QtQuick.Controls 2.15
+import QtGraphicalEffects 1.15
+
+Window {
+ id: root
+ color: "transparent"
+ flags: Qt.Dialog | Qt.FramelessWindowHint
+
+ readonly property int windowSpacing: 10
+ readonly property int windowWidth: 240
+
+ readonly property string svgImage: "image://svgimage-custom-color/%1.svg" + "/"
+ readonly property string talkIcon: svgImage.arg("wizard-talk")
+ readonly property string deleteIcon: svgImage.arg("delete")
+
+ // We set talkNotificationData, subject, and links properties in C++
+ property var talkNotificationData: ({})
+ property string subject: ""
+ property var links: []
+ property string link: ""
+ property string ringtonePath: "qrc:///client/theme/call-notification.wav"
+
+ readonly property bool usingUserAvatar: root.talkNotificationData.userAvatar !== ""
+
+ function closeNotification() {
+ ringSound.stop();
+ root.close();
+ }
+
+ width: root.windowWidth
+ height: rootBackground.height
+
+ Component.onCompleted: {
+ Systray.forceWindowInit(root);
+ Systray.positionNotificationWindow(root);
+
+ root.show();
+ root.raise();
+ root.requestActivate();
+
+ ringSound.play();
+ }
+
+ Audio {
+ id: ringSound
+ source: root.ringtonePath
+ loops: 9 // about 45 seconds of audio playing
+ audioRole: Audio.RingtoneRole
+ onStopped: root.closeNotification()
+ }
+
+ Rectangle {
+ id: rootBackground
+ width: parent.width
+ height: contentLayout.height + (root.windowSpacing * 2)
+ radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius
+ color: Style.backgroundColor
+ border.width: Style.trayWindowBorderWidth
+ border.color: Style.menuBorder
+ clip: true
+
+ Loader {
+ id: backgroundLoader
+ anchors.fill: parent
+ active: root.usingUserAvatar
+ sourceComponent: Item {
+ anchors.fill: parent
+
+ Image {
+ id: backgroundImage
+ anchors.fill: parent
+ cache: true
+ source: root.talkNotificationData.userAvatar
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ visible: false
+ }
+
+ FastBlur {
+ id: backgroundBlur
+ anchors.fill: backgroundImage
+ source: backgroundImage
+ radius: 50
+ visible: false
+ }
+
+ Rectangle {
+ id: backgroundMask
+ color: "white"
+ radius: rootBackground.radius
+ anchors.fill: backgroundImage
+ visible: false
+ width: backgroundImage.paintedWidth
+ height: backgroundImage.paintedHeight
+ }
+
+ OpacityMask {
+ id: backgroundOpacityMask
+ anchors.fill: backgroundBlur
+ source: backgroundBlur
+ maskSource: backgroundMask
+ }
+
+ Rectangle {
+ id: darkenerRect
+ anchors.fill: parent
+ color: "black"
+ opacity: 0.4
+ visible: backgroundOpacityMask.visible
+ radius: rootBackground.radius
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: contentLayout
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: root.windowSpacing
+ spacing: root.windowSpacing
+
+ Item {
+ width: Style.accountAvatarSize
+ height: Style.accountAvatarSize
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+
+ Image {
+ id: callerAvatar
+ anchors.fill: parent
+ cache: true
+
+ source: root.usingUserAvatar ? root.talkNotificationData.userAvatar :
+ Theme.darkMode ? root.talkIcon + Style.ncTextColor : root.talkIcon + Style.ncBlue
+ sourceSize.width: Style.accountAvatarSize
+ sourceSize.height: Style.accountAvatarSize
+
+ visible: !root.usingUserAvatar
+
+ Accessible.role: Accessible.Indicator
+ Accessible.name: qsTr("Talk notification caller avatar")
+ }
+
+ Rectangle {
+ id: mask
+ color: "white"
+ radius: width * 0.5
+ anchors.fill: callerAvatar
+ visible: false
+ width: callerAvatar.paintedWidth
+ height: callerAvatar.paintedHeight
+ }
+
+ OpacityMask {
+ anchors.fill: callerAvatar
+ source: callerAvatar
+ maskSource: mask
+ visible: root.usingUserAvatar
+ }
+ }
+
+ Label {
+ id: message
+ text: root.subject
+ color: root.usingUserAvatar ? "white" : Style.ncTextColor
+ font.pixelSize: Style.topLinePixelSize
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ Layout.fillWidth: true
+ }
+
+ RowLayout {
+ spacing: root.windowSpacing / 2
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+
+ Repeater {
+ id: linksRepeater
+ model: root.links
+
+ CustomButton {
+ id: answerCall
+ readonly property string verb: modelData.verb
+ readonly property bool isAnswerCallButton: verb === "WEB"
+
+ visible: isAnswerCallButton
+ text: modelData.label
+ bold: true
+ bgColor: Style.ncBlue
+ bgOpacity: 0.8
+
+ textColor: Style.ncHeaderTextColor
+
+ imageSource: root.talkIcon + Style.ncHeaderTextColor
+ imageSourceHover: root.talkIcon + Style.ncHeaderTextColor
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.callNotificationPrimaryButtonMinHeight
+
+ onClicked: {
+ Qt.openUrlExternally(root.link);
+ root.closeNotification();
+ }
+
+ Accessible.role: Accessible.Button
+ Accessible.name: qsTr("Answer Talk call notification")
+ Accessible.onPressAction: answerCall.clicked()
+ }
+
+ }
+
+ CustomButton {
+ id: declineCall
+ text: qsTr("Decline")
+ bold: true
+ bgColor: Style.errorBoxBackgroundColor
+ bgOpacity: 0.8
+
+ textColor: Style.ncHeaderTextColor
+
+ imageSource: root.deleteIcon + "white"
+ imageSourceHover: root.deleteIcon + "white"
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.callNotificationPrimaryButtonMinHeight
+
+ onClicked: root.closeNotification()
+
+ Accessible.role: Accessible.Button
+ Accessible.name: qsTr("Decline Talk call notification")
+ Accessible.onPressAction: declineCall.clicked()
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/gui/tray/CustomButton.qml b/src/gui/tray/CustomButton.qml
index 4c96c4b25..0ddb40d24 100644
--- a/src/gui/tray/CustomButton.qml
+++ b/src/gui/tray/CustomButton.qml
@@ -8,19 +8,22 @@ Button {
property string imageSource: ""
property string imageSourceHover: ""
+ property Image iconItem: icon
property string toolTipText: ""
- property color textColor
- property color textColorHovered
+ property color textColor: Style.ncTextColor
+ property color textColorHovered: textColor
property color bgColor: "transparent"
property bool bold: false
+ property real bgOpacity: 0.3
+
background: Rectangle {
color: root.bgColor
- opacity: parent.hovered ? 1.0 : 0.3
+ opacity: parent.hovered ? 1.0 : bgOpacity
radius: width / 2
}
@@ -49,6 +52,7 @@ Button {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
source: root.hovered ? root.imageSourceHover : root.imageSource
+ fillMode: Image.PreserveAspectFit
}
Label {
diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h
index 2ec77dadf..501b7fc68 100644
--- a/src/gui/tray/activitydata.h
+++ b/src/gui/tray/activitydata.h
@@ -33,7 +33,7 @@ namespace OCC {
class ActivityLink
{
Q_GADGET
-
+
Q_PROPERTY(QString imageSource MEMBER _imageSource)
Q_PROPERTY(QString imageSourceHovered MEMBER _imageSourceHovered)
Q_PROPERTY(QString label MEMBER _label)
@@ -115,6 +115,7 @@ public:
QString conversationToken;
QString messageId;
QString messageSent;
+ QString userAvatar;
};
Type _type;
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
index 20badd5dc..d01bb1c7e 100644
--- a/src/gui/tray/activitylistmodel.cpp
+++ b/src/gui/tray/activitylistmodel.cpp
@@ -80,6 +80,7 @@ QHash ActivityListModel::roleNames() const
roles[TalkNotificationConversationTokenRole] = "conversationToken";
roles[TalkNotificationMessageIdRole] = "messageId";
roles[TalkNotificationMessageSentRole] = "messageSent";
+ roles[TalkNotificationUserAvatarRole] = "userAvatar";
return roles;
}
@@ -332,6 +333,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return a._talkNotificationData.messageId;
case TalkNotificationMessageSentRole:
return replyMessageSent(a);
+ case TalkNotificationUserAvatarRole:
+ return a._talkNotificationData.userAvatar;
default:
return QVariant();
}
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
index 1a421861e..43cc211b5 100644
--- a/src/gui/tray/activitylistmodel.h
+++ b/src/gui/tray/activitylistmodel.h
@@ -71,6 +71,7 @@ public:
TalkNotificationConversationTokenRole,
TalkNotificationMessageIdRole,
TalkNotificationMessageSentRole,
+ TalkNotificationUserAvatarRole,
};
Q_ENUM(DataRole)
diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp
index 9246486e8..f274b80b9 100644
--- a/src/gui/tray/notificationhandler.cpp
+++ b/src/gui/tray/notificationhandler.cpp
@@ -90,13 +90,30 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
auto *ai = qvariant_cast(sender()->property(propertyAccountStateC));
ActivityList list;
+ ActivityList callList;
+
foreach (auto element, notifies) {
auto json = element.toObject();
auto a = Activity::fromActivityJson(json, ai->account());
+
a._type = Activity::NotificationType;
a._id = json.value("notification_id").toInt();
+ if(json.contains("subjectRichParameters")) {
+ const auto richParams = json.value("subjectRichParameters").toObject();
+ for(const auto &key : richParams.keys()) {
+ const auto parameterJsonObject = richParams.value(key).toObject();
+ a._subjectRichParameters.insert(key, Activity::RichSubjectParameter{
+ parameterJsonObject.value(QStringLiteral("type")).toString(),
+ parameterJsonObject.value(QStringLiteral("id")).toString(),
+ parameterJsonObject.value(QStringLiteral("name")).toString(),
+ QString(),
+ QUrl()
+ });
+ }
+ }
+
// 2 cases to consider:
// 1. server == 24 & has Talk: object_type is chat/call/room & object_id contains conversationToken/messageId
// 2. server < 24 & has Talk: object_type is chat/call/room & object_id contains _only_ conversationToken
@@ -116,7 +133,16 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
al._primary = true;
a._links.insert(0, al);
+ if(a._subjectRichParameters.contains("user")) {
+ a._talkNotificationData.userAvatar = ai->account()->url().toString() + QStringLiteral("/index.php/avatar/") + a._subjectRichParameters["user"].id + QStringLiteral("/128");
+ }
+
list.append(a);
+
+ // We want to serve incoming call dialogs to the user for calls that
+ if(a._objectType == "call" && a._dateTime.secsTo(QDateTime::currentDateTime()) < 120) {
+ callList.append(a);
+ }
}
a._status = 0;
@@ -145,6 +171,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
list.append(a);
}
emit newNotificationList(list);
+ emit newIncomingCallsList(callList);
deleteLater();
}
diff --git a/src/gui/tray/notificationhandler.h b/src/gui/tray/notificationhandler.h
index 3f2622167..fc039137c 100644
--- a/src/gui/tray/notificationhandler.h
+++ b/src/gui/tray/notificationhandler.h
@@ -17,6 +17,7 @@ public:
signals:
void newNotificationList(ActivityList);
+ void newIncomingCallsList(ActivityList);
public slots:
void slotFetchNotifications();
diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp
index fe80eeae5..7bc10a1fb 100644
--- a/src/gui/tray/usermodel.cpp
+++ b/src/gui/tray/usermodel.cpp
@@ -12,6 +12,7 @@
#include "logger.h"
#include "guiutility.h"
#include "syncfileitem.h"
+#include "systray.h"
#include "tray/activitylistmodel.h"
#include "tray/notificationcache.h"
#include "tray/unifiedsearchresultslistmodel.h"
@@ -123,6 +124,18 @@ void User::slotBuildNotificationDisplay(const ActivityList &list)
}
}
+void User::slotBuildIncomingCallDialogs(const ActivityList &list)
+{
+ const auto systray = Systray::instance();
+ const ConfigFile cfg;
+
+ if(systray && cfg.showCallNotifications()) {
+ for(const auto &activity : list) {
+ systray->createCallDialog(activity);
+ }
+ }
+}
+
void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
{
if (!checkPushNotificationsAreReady()) {
@@ -264,6 +277,8 @@ void User::slotRefreshNotifications()
auto *snh = new ServerNotificationHandler(_account.data());
connect(snh, &ServerNotificationHandler::newNotificationList,
this, &User::slotBuildNotificationDisplay);
+ connect(snh, &ServerNotificationHandler::newIncomingCallsList,
+ this, &User::slotBuildIncomingCallDialogs);
snh->slotFetchNotifications();
} else {
@@ -906,7 +921,7 @@ void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
endInsertRows();
ConfigFile cfg;
- _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
+ u->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
emit newUserSelected();
}
}
diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h
index 81036f940..c69e7827b 100644
--- a/src/gui/tray/usermodel.h
+++ b/src/gui/tray/usermodel.h
@@ -100,6 +100,7 @@ public slots:
void slotNotifyServerFinished(const QString &reply, int replyCode);
void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
void slotBuildNotificationDisplay(const ActivityList &list);
+ void slotBuildIncomingCallDialogs(const ActivityList &list);
void slotRefreshNotifications();
void slotRefreshActivities();
void slotRefresh();
diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp
index 0f2059be2..79ea92e83 100644
--- a/src/libsync/configfile.cpp
+++ b/src/libsync/configfile.cpp
@@ -68,6 +68,7 @@ static const char monoIconsC[] = "monoIcons";
static const char promptDeleteC[] = "promptDeleteAllFiles";
static const char crashReporterC[] = "crashReporter";
static const char optionalServerNotificationsC[] = "optionalServerNotifications";
+static const char showCallNotificationsC[] = "showCallNotifications";
static const char showInExplorerNavigationPaneC[] = "showInExplorerNavigationPane";
static const char skipUpdateCheckC[] = "skipUpdateCheck";
static const char autoUpdateCheckC[] = "autoUpdateCheck";
@@ -189,6 +190,19 @@ bool ConfigFile::optionalServerNotifications() const
return settings.value(QLatin1String(optionalServerNotificationsC), true).toBool();
}
+bool ConfigFile::showCallNotifications() const
+{
+ const QSettings settings(configFile(), QSettings::IniFormat);
+ return settings.value(QLatin1String(showCallNotificationsC), true).toBool() && optionalServerNotifications();
+}
+
+void ConfigFile::setShowCallNotifications(bool show)
+{
+ QSettings settings(configFile(), QSettings::IniFormat);
+ settings.setValue(QLatin1String(showCallNotificationsC), show);
+ settings.sync();
+}
+
bool ConfigFile::showInExplorerNavigationPane() const
{
const bool defaultValue =
@@ -557,7 +571,7 @@ chrono::milliseconds ConfigFile::notificationRefreshInterval(const QString &conn
QSettings settings(configFile(), QSettings::IniFormat);
settings.beginGroup(con);
- auto defaultInterval = chrono::minutes(5);
+ const auto defaultInterval = chrono::minutes(1);
auto interval = millisecondsValue(settings, notificationRefreshIntervalC, defaultInterval);
if (interval < chrono::minutes(1)) {
qCWarning(lcConfigFile) << "Notification refresh interval smaller than one minute, setting to one minute";
diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h
index fb52c02c9..cf5716770 100644
--- a/src/libsync/configfile.h
+++ b/src/libsync/configfile.h
@@ -152,6 +152,9 @@ public:
bool optionalServerNotifications() const;
void setOptionalServerNotifications(bool show);
+ bool showCallNotifications() const;
+ void setShowCallNotifications(bool show);
+
bool showInExplorerNavigationPane() const;
void setShowInExplorerNavigationPane(bool show);
diff --git a/theme.qrc.in b/theme.qrc.in
index 512dd3508..1a5df733a 100644
--- a/theme.qrc.in
+++ b/theme.qrc.in
@@ -232,5 +232,6 @@
theme/black/edit.svg
theme/delete.svg
theme/send.svg
+ theme/call-notification.wav
diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml
index ad1da99d8..af7627ade 100644
--- a/theme/Style/Style.qml
+++ b/theme/Style/Style.qml
@@ -30,7 +30,7 @@ QtObject {
property int trayWindowWidth: variableSize(400)
property int trayWindowHeight: variableSize(510)
property int trayWindowRadius: 10
- property int trayWindowBorderWidth: 1
+ property int trayWindowBorderWidth: variableSize(1)
property int trayWindowHeaderHeight: variableSize(60)
property int trayHorizontalMargin: 10
property int trayListItemIconSize: accountAvatarSize
@@ -68,6 +68,9 @@ QtObject {
property int activityItemActionPrimaryButtonMinWidth: 100
property int activityItemActionSecondaryButtonMinWidth: 80
+ property int callNotificationPrimaryButtonMinWidth: 100
+ property int callNotificationPrimaryButtonMinHeight: 40
+
property int roundButtonBackgroundVerticalMargins: 10
property int roundedButtonBackgroundVerticalMargins: 5
diff --git a/theme/call-notification.wav b/theme/call-notification.wav
new file mode 100644
index 000000000..cceae103e
Binary files /dev/null and b/theme/call-notification.wav differ