Переглянути джерело

Add TalkReply class and tests.

- Add struct TalkNotificationData to handle token and messageId.
- Handle chat and call notifications with the new struct.
- Add talk token and messageId to data roles in ActivityListModel.
- Add Talk Reply component to the ActivityList.
- User Loader to display the TalkReply component.
- Move Talk Reply from ActivityItem to ActivityItemContent due to PR #4186.
- Use TextField instead of Text.
- Disable send reply button instead of changing border color when field is empty.

Signed-off-by: Camila <hello@camila.codes>
Camila 4 роки тому
батько
коміт
73bae8cd30

+ 1 - 0
resources.qrc

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

+ 1 - 1
src/gui/CMakeLists.txt

@@ -207,6 +207,7 @@ set(client_SRCS
     tray/notificationcache.h
     tray/notificationcache.cpp
     creds/credentialsfactory.h
+    tray/talkreply.cpp
     creds/credentialsfactory.cpp
     creds/httpcredentialsgui.h
     creds/httpcredentialsgui.cpp
@@ -392,7 +393,6 @@ function(generate_sized_png_from_svg icon_path size)
   if (ARG_OUTPUT_ICON_PATH)
     set(icon_name_dir ${ARG_OUTPUT_ICON_PATH})
   endif ()
-  
 
   if (EXISTS "${icon_name_dir}/${size}-${icon_name_wle}.png")
     return()

+ 1 - 0
src/gui/systray.h

@@ -91,6 +91,7 @@ signals:
     void showWindow();
     void openShareDialog(const QString &sharePath, const QString &localPath);
     void showFileActivityDialog(const QString &objectName, const int objectId);
+    void sendChatMessage(const QString &token, const QString &message, const QString &replyTo);
 
 public slots:
     void slotNewUserSelected();

+ 3 - 1
src/gui/tray/ActivityItem.qml

@@ -12,7 +12,8 @@ MouseArea {
 
     property bool isFileActivityList: false
 
-    property bool isChatActivity: model.objectType === "chat" || model.objectType === "room"
+    property bool isChatActivity: model.objectType === "chat" || model.objectType === "room" || model.objectType === "call"
+    property bool isTalkReplyPossible: model.conversationToken !== ""
 
     signal fileActivityButtonClicked(string absolutePath)
 
@@ -67,6 +68,7 @@ MouseArea {
             Layout.fillWidth: true
             Layout.leftMargin: 40
             Layout.bottomMargin: model.links.length > 1 ? 5 : 0
+            Layout.topMargin: isTalkReplyPossible? 48 : 0
 
             displayActions: model.displayActions
             objectType: model.objectType

+ 15 - 2
src/gui/tray/ActivityItemContent.qml

@@ -126,9 +126,22 @@ RowLayout {
             maximumLineCount: 2
             font.pixelSize: Style.subLinePixelSize
             color: "#808080"
-        }
-    }
+        }  
+
+        Loader {
+            id: talkReplyTextFieldLoader
+            active: isChatActivity && isTalkReplyPossible
 
+            anchors.top: activityTextDateTime.bottom
+            anchors.topMargin: 10
+
+            sourceComponent: TalkReplyTextField {
+                id: talkReplyMessage
+                anchors.fill: parent
+            }
+        }         
+    }    
+    
     Button {
         id: dismissActionButton
 

+ 76 - 0
src/gui/tray/TalkReplyTextField.qml

@@ -0,0 +1,76 @@
+import QtQuick 2.15
+import Style 1.0
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import com.nextcloud.desktopclient 1.0
+
+Item {
+    id: root
+
+    function sendReplyMessage() {
+        if (replyMessageTextField.text === "") {
+            return;
+        }
+
+        UserModel.currentUser.sendReplyMessage(model.conversationToken, replyMessageTextField.text, model.messageId);
+        replyMessageSent.text = replyMessageTextField.text;
+        replyMessageTextField.clear();
+    }
+
+    Text {
+        id: replyMessageSent
+        font.pixelSize: Style.topLinePixelSize
+        color: Style.menuBorder
+        visible: replyMessageSent.text !== ""
+    }
+
+    TextField {
+        id: replyMessageTextField
+
+        // TODO use Layout to manage width/height. The Layout.minimunWidth does not apply to the width set.
+        height: 38
+        width: 250
+
+        onAccepted: root.sendReplyMessage()
+        visible: replyMessageSent.text === ""
+
+        topPadding: 4
+
+        placeholderText: qsTr("Reply to …")
+
+        background: Rectangle {
+            id: replyMessageTextFieldBorder
+            radius: 24
+            border.width: 1
+            border.color: Style.ncBlue
+        }
+
+        Button {
+            id: sendReplyMessageButton  
+            width: 32
+            height: parent.height
+            opacity: 0.8
+            flat: true
+            enabled: replyMessageTextField.text !== ""
+            onClicked: root.sendReplyMessage()
+
+            icon {
+                source: "image://svgimage-custom-color/send.svg" + "/" + Style.ncBlue
+                width: 38
+                height: 38
+                color: hovered || !sendReplyMessageButton.enabled? Style.menuBorder : Style.ncBlue
+            }
+
+            anchors {
+                right: replyMessageTextField.right
+                top: replyMessageTextField.top
+            }
+
+            ToolTip {
+                visible: sendReplyMessageButton.hovered
+                delay: Qt.styleHints.mousePressAndHoldInterval
+                text:  qsTr("Send reply to chat message")
+            }
+        }
+    }
+}

+ 6 - 0
src/gui/tray/activitydata.h

@@ -109,10 +109,16 @@ public:
         QUrl link;    // Optional (files only)
     };
 
+    struct TalkNotificationData {
+        QString conversationToken;
+        QString messageId;
+    };
+
     Type _type;
     qlonglong _id;
     QString _fileAction;
     int _objectId;
+    TalkNotificationData _talkNotificationData;
     QString _objectType;
     QString _objectName;
     QString _subject;

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

@@ -75,6 +75,9 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
     roles[ShareableRole] = "isShareable";
     roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
     roles[ThumbnailRole] = "thumbnail";
+    roles[TalkConversationTokenRole] = "conversationToken";
+    roles[TalkMessageIdRole] = "messageId";
+
     return roles;
 }
 
@@ -310,6 +313,10 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         const auto preview = a._previews[0];
         return(generatePreviewMap(preview));
     }
+    case TalkConversationTokenRole:
+        return a._talkNotificationData.conversationToken;
+    case TalkMessageIdRole:
+        return a._talkNotificationData.messageId;
     default:
         return QVariant();
     }

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

@@ -67,6 +67,8 @@ public:
         ShareableRole,
         IsCurrentUserFileActivityRole,
         ThumbnailRole,
+        TalkConversationTokenRole,
+        TalkMessageIdRole,
     };
     Q_ENUM(DataRole)
 

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

@@ -100,6 +100,21 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
 
         //need to know, specially for remote_share
         a._objectType = json.value("object_type").toString();
+
+        // 2 cases to consider:
+        // - server == 24 & has Talk: notification type chat/call contains conversationToken/messageId in object_type
+        // - server < 24 & has Talk: notification type chat/call contains _only_ the conversationToken in object_type
+        if (a._objectType == "chat" || a._objectType == "call") {
+            const auto objectId = json.value("object_id").toString();
+            const auto objectIdData = objectId.split("/");
+            a._talkNotificationData.conversationToken = objectIdData.first();
+            if (a._objectType == "chat" && objectIdData.size() > 1) {
+                a._talkNotificationData.messageId = objectIdData.last();
+            } else {
+                qCInfo(lcServerNotification) << "Replying directly to Talk conversation" << a._talkNotificationData.conversationToken << "will not be possible because the notification doesn't contain the message ID.";
+            }
+        } 
+
         a._status = 0;
 
         a._subject = json.value("subject").toString();

+ 44 - 0
src/gui/tray/talkreply.cpp

@@ -0,0 +1,44 @@
+#include "talkreply.h"
+#include "accountstate.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcTalkReply, "nextcloud.gui.talkreply", QtInfoMsg)
+
+TalkReply::TalkReply(AccountState *accountState, QObject *parent)
+    : QObject(parent)
+    , _accountState(accountState)
+{
+    Q_ASSERT(_accountState && _accountState->account());
+}
+
+void TalkReply::sendReplyMessage(const QString &conversationToken, const QString &message, const QString &replyTo)
+{
+    QPointer<JsonApiJob> apiJob =  new JsonApiJob(_accountState->account(),
+        QLatin1String("ocs/v2.php/apps/spreed/api/v1/chat/%1").arg(conversationToken),
+        this);
+
+    QObject::connect(apiJob, &JsonApiJob::jsonReceived, this, [&](const QJsonDocument &response, const int statusCode) {
+        if(statusCode != 200) {
+            qCWarning(lcTalkReply) << "Status code" << statusCode;
+        }
+
+        const auto responseObj = response.object().value("ocs").toObject().value("data").toObject();
+        emit replyMessageSent(responseObj.value("message").toString());
+
+        deleteLater();
+    });
+
+    QUrlQuery params;
+    params.addQueryItem(QStringLiteral("message"), message);
+    params.addQueryItem(QStringLiteral("replyTo"), QString(replyTo));
+
+    apiJob->addQueryParams(params);
+    apiJob->setVerb(JsonApiJob::Verb::Post);
+    apiJob->start();
+}
+}

+ 24 - 0
src/gui/tray/talkreply.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include <QtCore>
+#include <QPointer>
+
+namespace OCC {
+class AccountState;
+
+class TalkReply : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit TalkReply(AccountState *accountState, QObject *parent = nullptr);
+
+    void sendReplyMessage(const QString &conversationToken, const QString &message, const QString &replyTo = {});
+
+signals:
+    void replyMessageSent(const QString &message);
+
+private:
+    AccountState *_accountState = nullptr;
+};
+}

+ 9 - 0
src/gui/tray/usermodel.cpp

@@ -15,6 +15,7 @@
 #include "tray/activitylistmodel.h"
 #include "tray/notificationcache.h"
 #include "tray/unifiedsearchresultslistmodel.h"
+#include "tray/talkreply.h"
 #include "userstatusconnector.h"
 #include "thumbnailjob.h"
 
@@ -79,6 +80,8 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
     connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::accentColorChanged);
 
     connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
+    
+    connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage);
 }
 
 void User::showDesktopNotification(const QString &title, const QString &message)
@@ -785,6 +788,12 @@ void User::removeAccount() const
     AccountManager::instance()->save();
 }
 
+void User::slotSendReplyMessage(const QString &token, const QString &message, const QString &replyTo)
+{
+    QPointer<TalkReply> talkReply = new TalkReply(_account.data(), this);
+    talkReply->sendReplyMessage(token, message, replyTo);
+}
+
 /*-------------------------------------------------------------------------------------*/
 
 UserModel *UserModel::_instance = nullptr;

+ 4 - 0
src/gui/tray/usermodel.h

@@ -38,6 +38,7 @@ class User : public QObject
     Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
     Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
     Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
+
 public:
     User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
 
@@ -86,6 +87,7 @@ signals:
     void headerColorChanged();
     void headerTextColorChanged();
     void accentColorChanged();
+    void sendReplyMessage(const QString &token, const QString &message, const QString &replyTo);
 
 public slots:
     void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
@@ -105,6 +107,7 @@ public slots:
     void slotRefreshImmediately();
     void setNotificationRefreshInterval(std::chrono::milliseconds interval);
     void slotRebuildNavigationAppList();
+    void slotSendReplyMessage(const QString &conversationToken, const QString &message, const QString &replyTo);
 
 private:
     void slotPushNotificationsReady();
@@ -139,6 +142,7 @@ private:
     // number of currently running notification requests. If non zero,
     // no query for notifications is started.
     int _notificationRequestsRunning;
+    QString textSentStr;
 };
 
 class UserModel : public QAbstractListModel

+ 1 - 0
test/CMakeLists.txt

@@ -63,6 +63,7 @@ nextcloud_add_test(SetUserStatusDialog)
 nextcloud_add_test(UnifiedSearchListmodel)
 nextcloud_add_test(ActivityListModel)
 nextcloud_add_test(ActivityData)
+nextcloud_add_test(TalkReply)
 
 if( UNIX AND NOT APPLE )
     nextcloud_add_test(InotifyWatcher)

Різницю між файлами не показано, бо вона завелика
+ 15 - 0
test/testtalkreply.cpp


+ 1 - 0
theme.qrc.in

@@ -213,5 +213,6 @@
         <file>theme/black/email.svg</file>
         <file>theme/black/edit.svg</file>
         <file>theme/delete.svg</file>
+        <file>theme/send.svg</file>
     </qresource>
 </RCC>

+ 1 - 0
theme/send.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>

Деякі файли не було показано, через те що забагато файлів було змінено