diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 96a2ff09..c5269f32 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -78,6 +78,29 @@ void Notification::close(NotificationCloseReason::Enum reason) { } } +void Notification::sendInlineReply(const QString& replyText) { + if (!NotificationServer::instance()->support.inlineReply) { + qCritical() << "Inline reply support disabled on server"; + return; + } + + if (!this->bHasInlineReply) { + qCritical() << "Cannot send reply to notification without inline-reply action"; + return; + } + + if (this->isRetained()) { + qCritical() << "Cannot send reply to destroyed notification" << this; + return; + } + + NotificationServer::instance()->NotificationReplied(this->id(), replyText); + + if (!this->bindableResident().value()) { + this->close(NotificationCloseReason::Dismissed); + } +} + void Notification::updateProperties( const QString& appName, QString appIcon, @@ -147,17 +170,27 @@ void Notification::updateProperties( this->bImage = imagePath; this->bHints = hints; - Qt::endPropertyUpdateGroup(); - bool actionsChanged = false; auto deletedActions = QVector(); if (actions.length() % 2 == 0) { int ai = 0; for (auto i = 0; i != actions.length(); i += 2) { - ai = i / 2; const auto& identifier = actions.at(i); const auto& text = actions.at(i + 1); + + if (identifier == "inline-reply" && NotificationServer::instance()->support.inlineReply) { + if (this->bHasInlineReply) { + qCWarning(logNotifications) << this << '(' << appName << ')' + << "sent an action set with duplicate inline-reply actions."; + } else { + this->bHasInlineReply = true; + this->bInlineReplyPlaceholder = text; + } + // skip inserting this action into action list + continue; + } + auto* action = ai < this->mActions.length() ? this->mActions.at(ai) : nullptr; if (action && identifier == action->identifier()) { @@ -188,6 +221,8 @@ void Notification::updateProperties( << "sent an action set of an invalid length."; } + Qt::endPropertyUpdateGroup(); + if (actionsChanged) emit this->actionsChanged(); for (auto* action: deletedActions) { diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index f0c65bbb..06c871bb 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -107,6 +107,12 @@ class Notification /// /// This image is often something like a profile picture in instant messaging applications. Q_PROPERTY(QString image READ default NOTIFY imageChanged BINDABLE bindableImage); + /// If true, the notification has an inline reply action. + /// + /// A quick reply text field should be displayed and the reply can be sent using @@sendInlineReply(). + Q_PROPERTY(bool hasInlineReply READ default NOTIFY hasInlineReplyChanged BINDABLE bindableHasInlineReply); + /// The placeholder text/button caption for the inline reply. + Q_PROPERTY(QString inlineReplyPlaceholder READ default NOTIFY inlineReplyPlaceholderChanged BINDABLE bindableInlineReplyPlaceholder); /// All hints sent by the client application as a javascript object. /// Many common hints are exposed via other properties. Q_PROPERTY(QVariantMap hints READ default NOTIFY hintsChanged BINDABLE bindableHints); @@ -124,6 +130,12 @@ public: /// explicitly closed by the user. Q_INVOKABLE void dismiss(); + /// Send an inline reply to the notification with an inline reply action. + /// > [!WARNING] This method can only be called if + /// > @@hasInlineReply is true + /// > and the server has @@NotificationServer.inlineReplySupported set to true. + Q_INVOKABLE void sendInlineReply(const QString& replyText); + void updateProperties( const QString& appName, QString appIcon, @@ -158,6 +170,8 @@ public: [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; }; [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; [[nodiscard]] QBindable bindableImage() const { return &this->bImage; }; + [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; }; + [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { return &this->bInlineReplyPlaceholder; }; [[nodiscard]] QBindable bindableHints() const { return &this->bHints; }; [[nodiscard]] NotificationCloseReason::Enum closeReason() const; @@ -182,6 +196,8 @@ signals: void transientChanged(); void desktopEntryChanged(); void imageChanged(); + void hasInlineReplyChanged(); + void inlineReplyPlaceholderChanged(); void hintsChanged(); private: @@ -202,6 +218,8 @@ private: Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bTransient, &Notification::transientChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bDesktopEntry, &Notification::desktopEntryChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bImage, &Notification::imageChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bHasInlineReply, &Notification::hasInlineReplyChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bInlineReplyPlaceholder, &Notification::inlineReplyPlaceholderChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QVariantMap, bHints, &Notification::hintsChanged); // clang-format on diff --git a/src/services/notifications/org.freedesktop.Notifications.xml b/src/services/notifications/org.freedesktop.Notifications.xml index 1a2001fe..3d99db09 100644 --- a/src/services/notifications/org.freedesktop.Notifications.xml +++ b/src/services/notifications/org.freedesktop.Notifications.xml @@ -38,6 +38,11 @@ + + + + + diff --git a/src/services/notifications/qml.cpp b/src/services/notifications/qml.cpp index 99818214..42bb23a0 100644 --- a/src/services/notifications/qml.cpp +++ b/src/services/notifications/qml.cpp @@ -115,6 +115,15 @@ void NotificationServerQml::setImageSupported(bool imageSupported) { emit this->imageSupportedChanged(); } +bool NotificationServerQml::inlineReplySupported() const { return this->support.inlineReply; } + +void NotificationServerQml::setInlineReplySupported(bool inlineReplySupported) { + if (inlineReplySupported == this->support.inlineReply) return; + this->support.inlineReply = inlineReplySupported; + this->updateSupported(); + emit this->inlineReplySupportedChanged(); +} + QVector NotificationServerQml::extraHints() const { return this->support.extraHints; } void NotificationServerQml::setExtraHints(QVector extraHints) { diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index feb33db3..88132c70 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -65,6 +65,8 @@ class NotificationServerQml: public PostReloadHook { Q_PROPERTY(bool actionIconsSupported READ actionIconsSupported WRITE setActionIconsSupported NOTIFY actionIconsSupportedChanged); /// If the notification server should advertise that it supports images. Defaults to false. Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged); + /// If the notification server should advertise that it supports inline replies. Defaults to false. + Q_PROPERTY(bool inlineReplySupported READ inlineReplySupported WRITE setInlineReplySupported NOTIFY inlineReplySupportedChanged); /// All notifications currently tracked by the server. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); @@ -103,6 +105,9 @@ public: [[nodiscard]] bool imageSupported() const; void setImageSupported(bool imageSupported); + [[nodiscard]] bool inlineReplySupported() const; + void setInlineReplySupported(bool inlineReplySupported); + [[nodiscard]] QVector extraHints() const; void setExtraHints(QVector extraHints); @@ -123,6 +128,7 @@ signals: void actionsSupportedChanged(); void actionIconsSupportedChanged(); void imageSupportedChanged(); + void inlineReplySupportedChanged(); void extraHintsChanged(); void trackedNotificationsChanged(); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 18a898aa..ac1e9052 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -155,6 +155,7 @@ QStringList NotificationServer::GetCapabilities() const { } if (this->support.image) capabilities += "icon-static"; + if (this->support.inlineReply) capabilities += "inline-reply"; capabilities += this->support.extraHints; diff --git a/src/services/notifications/server.hpp b/src/services/notifications/server.hpp index 8c20943d..8bd92a34 100644 --- a/src/services/notifications/server.hpp +++ b/src/services/notifications/server.hpp @@ -23,6 +23,7 @@ struct NotificationServerSupport { bool actions = false; bool actionIcons = false; bool image = false; + bool inlineReply = false; QVector extraHints; }; @@ -60,6 +61,7 @@ signals: // NOLINTBEGIN void NotificationClosed(quint32 id, quint32 reason); void ActionInvoked(quint32 id, QString action); + void NotificationReplied(quint32 id, QString replyText); // NOLINTEND private slots: