service/notifications: add inline-reply action support

Signed-off-by: ipg0 <pyromancy00@gmail.com>
This commit is contained in:
ipg0 2025-07-15 00:15:55 +03:00 committed by outfoxxed
parent 3dfb7d8827
commit c40074dd56
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
7 changed files with 79 additions and 3 deletions

View file

@ -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<NotificationAction*>();
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) {

View file

@ -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<bool> bindableTransient() const { return &this->bTransient; };
[[nodiscard]] QBindable<QString> bindableDesktopEntry() const { return &this->bDesktopEntry; };
[[nodiscard]] QBindable<QString> bindableImage() const { return &this->bImage; };
[[nodiscard]] QBindable<bool> bindableHasInlineReply() const { return &this->bHasInlineReply; };
[[nodiscard]] QBindable<QString> bindableInlineReplyPlaceholder() const { return &this->bInlineReplyPlaceholder; };
[[nodiscard]] QBindable<QVariantMap> 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

View file

@ -38,6 +38,11 @@
<arg name="actionKey" type="s" direction="out"/>
</signal>
<signal name="NotificationReplied">
<arg name="id" type="u" direction="out"/>
<arg name="replyText" type="s" direction="out"/>
</signal>
<signal name="ActivationToken">
<arg name="id" type="u" direction="out"/>
<arg name="activationToken" type="s" direction="out"/>

View file

@ -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<QString> NotificationServerQml::extraHints() const { return this->support.extraHints; }
void NotificationServerQml::setExtraHints(QVector<QString> extraHints) {

View file

@ -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<qs::service::notifications::Notification>*);
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<QString> extraHints() const;
void setExtraHints(QVector<QString> extraHints);
@ -123,6 +128,7 @@ signals:
void actionsSupportedChanged();
void actionIconsSupportedChanged();
void imageSupportedChanged();
void inlineReplySupportedChanged();
void extraHintsChanged();
void trackedNotificationsChanged();

View file

@ -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;

View file

@ -23,6 +23,7 @@ struct NotificationServerSupport {
bool actions = false;
bool actionIcons = false;
bool image = false;
bool inlineReply = false;
QVector<QString> 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: