Sfoglia il codice sorgente

Add an incoming talk call notification to the desktop client

Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>

Co-authored-by: Camila <hello@camila.codes>
Claudio Cambra 3 anni fa
parent
commit
3f5243aaee

+ 1 - 0
resources.qrc

@@ -29,5 +29,6 @@
         <file>src/gui/tray/ActivityItemActions.qml</file>
         <file>src/gui/tray/ActivityItemContent.qml</file>
         <file>src/gui/tray/TalkReplyTextField.qml</file>
+        <file>src/gui/tray/CallNotificationDialog.qml</file>
     </qresource>
 </RCC>

+ 13 - 0
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)

+ 1 - 0
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();

+ 9 - 2
src/gui/generalsettings.ui

@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>554</width>
-    <height>558</height>
+    <width>556</width>
+    <height>563</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -90,6 +90,13 @@
         </property>
        </widget>
       </item>
+      <item row="1" column="1">
+       <widget class="QCheckBox" name="callNotificationsCheckBox">
+        <property name="text">
+         <string>Show Call Notifications</string>
+        </property>
+       </widget>
+      </item>
      </layout>
     </widget>
    </item>

+ 192 - 22
src/gui/systray.cpp

@@ -29,6 +29,7 @@
 #include <QQmlApplicationEngine>
 #include <QQmlContext>
 #include <QQuickWindow>
+#include <QVariantMap>
 #include <QScreen>
 #include <QMenu>
 #include <QGuiApplication>
@@ -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,44 +607,74 @@ 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);
-        }
+    const auto windowRect = computeWindowRect(spacing, topLeft, bottomRight);
 
-        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);
-        }
+    qCDebug(lcSystray) << "taskbarScreenEdge:" << taskbarScreenEdge;
+    qCDebug(lcSystray) << "screenRect:" << screenRect;
+    qCDebug(lcSystray) << "windowRect (reference)" << QRect(topLeft, bottomRight);
+    qCDebug(lcSystray) << "windowRect (adjusted)" << windowRect;
+
+    return windowRect.topLeft();
+}
+
+QPoint Systray::computeNotificationPosition(int width, int height, int spacing, NotificationPosition position) const
+{
+    const auto referencePoint = computeNotificationReferencePoint(spacing, position);
 
-        return rect.translated(offset);
+    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()

+ 10 - 0
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<QQmlApplicationEngine> _trayEngine;
 
     AccessManagerFactory _accessManagerFactory;
+
+    QSet<qlonglong> _callsAlreadyNotified;
 };
 
 } // namespace OCC

+ 1 - 0
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)
 

+ 244 - 0
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()
+                }
+            }
+        }
+
+    }
+}

+ 7 - 3
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 {

+ 2 - 1
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;

+ 3 - 0
src/gui/tray/activitylistmodel.cpp

@@ -80,6 +80,7 @@ QHash<int, QByteArray> 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();
     }

+ 1 - 0
src/gui/tray/activitylistmodel.h

@@ -71,6 +71,7 @@ public:
         TalkNotificationConversationTokenRole,
         TalkNotificationMessageIdRole,
         TalkNotificationMessageSentRole,
+        TalkNotificationUserAvatarRole,
     };
     Q_ENUM(DataRole)
 

+ 27 - 0
src/gui/tray/notificationhandler.cpp

@@ -90,13 +90,30 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
     auto *ai = qvariant_cast<AccountState *>(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();
 }

+ 1 - 0
src/gui/tray/notificationhandler.h

@@ -17,6 +17,7 @@ public:
 
 signals:
     void newNotificationList(ActivityList);
+    void newIncomingCallsList(ActivityList);
 
 public slots:
     void slotFetchNotifications();

+ 16 - 1
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();
     }
 }

+ 1 - 0
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();

+ 15 - 1
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";

+ 3 - 0
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);
 

+ 1 - 0
theme.qrc.in

@@ -232,5 +232,6 @@
         <file>theme/black/edit.svg</file>
         <file>theme/delete.svg</file>
         <file>theme/send.svg</file>
+        <file>theme/call-notification.wav</file>
     </qresource>
 </RCC>

+ 4 - 1
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
     

BIN
theme/call-notification.wav