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