Add thumbnails for files in the activity view
authorClaudio Cambra <claudio.cambra@gmail.com>
Thu, 20 Jan 2022 11:54:36 +0000 (12:54 +0100)
committerClaudio Cambra (Rebase PR Action) <claudio.cambra@gmail.com>
Thu, 17 Mar 2022 10:46:09 +0000 (10:46 +0000)
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
27 files changed:
src/gui/CMakeLists.txt
src/gui/systray.cpp
src/gui/tray/ActivityItem.qml
src/gui/tray/ActivityItemContent.qml
src/gui/tray/SyncStatus.qml
src/gui/tray/UnifiedSearchInputContainer.qml
src/gui/tray/UnifiedSearchResultItem.qml
src/gui/tray/Window.qml
src/gui/tray/activitydata.cpp
src/gui/tray/activitydata.h
src/gui/tray/activitylistmodel.cpp
src/gui/tray/activitylistmodel.h
src/gui/tray/asyncimageresponse.cpp [new file with mode: 0644]
src/gui/tray/asyncimageresponse.h [new file with mode: 0644]
src/gui/tray/trayimageprovider.cpp [new file with mode: 0644]
src/gui/tray/trayimageprovider.h [new file with mode: 0644]
src/gui/tray/unifiedsearchresultimageprovider.cpp [deleted file]
src/gui/tray/unifiedsearchresultimageprovider.h [deleted file]
src/gui/tray/usermodel.cpp
src/gui/tray/usermodel.h
test/CMakeLists.txt
test/testactivitydata.cpp [new file with mode: 0644]
theme.qrc.in
theme/Style/Style.qml
theme/colored/add-bordered.svg [new file with mode: 0644]
theme/colored/change-bordered.svg [new file with mode: 0644]
theme/colored/delete-bordered.svg [new file with mode: 0644]

index 6b3cf688e4f081753152d2b3bdeb9310b05b971d..3d1075a4bab9332a656bad96689c2498e7669011 100644 (file)
@@ -195,10 +195,10 @@ set(client_SRCS
     tray/activitylistmodel.h
     tray/activitylistmodel.cpp
     tray/unifiedsearchresult.h
+    tray/asyncimageresponse.cpp
     tray/unifiedsearchresult.cpp
-    tray/unifiedsearchresultimageprovider.h
-    tray/unifiedsearchresultimageprovider.cpp
     tray/unifiedsearchresultslistmodel.h
+    tray/trayimageprovider.cpp
     tray/unifiedsearchresultslistmodel.cpp
     tray/usermodel.h
     tray/usermodel.cpp
index 992343bd5349568e9d6e5d4229d2ab611977c425..a5a352dd47939d4c85b5125af2f69084e83e7dd7 100644 (file)
@@ -20,7 +20,7 @@
 #include "tray/svgimageprovider.h"
 #include "tray/usermodel.h"
 #include "wheelhandler.h"
-#include "tray/unifiedsearchresultimageprovider.h"
+#include "tray/trayimageprovider.h"
 #include "configfile.h"
 #include "accessmanager.h"
 
@@ -65,7 +65,7 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
     _trayEngine->addImportPath("qrc:/qml/theme");
     _trayEngine->addImageProvider("avatars", new ImageProvider);
     _trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider);
-    _trayEngine->addImageProvider(QLatin1String("unified-search-result-icon"), new UnifiedSearchResultImageProvider);
+    _trayEngine->addImageProvider(QLatin1String("tray-image-provider"), new TrayImageProvider);
 }
 
 Systray::Systray()
@@ -513,7 +513,11 @@ AccessManagerFactory::AccessManagerFactory()
 
 QNetworkAccessManager* AccessManagerFactory::create(QObject *parent)
 {
-    return new AccessManager(parent);
+    const auto am = new AccessManager(parent);
+    const auto diskCache = new QNetworkDiskCache(am);
+    diskCache->setCacheDirectory("cacheDir");
+    am->setCache(diskCache);
+    return am;
 }
 
 } // namespace OCC
index 54c272cfba6f7d069b98d81af9b919da7b9ce7fe..9c049529ab1fc7db97a06b38392d5df943618c20 100644 (file)
@@ -38,8 +38,8 @@ MouseArea {
     ColumnLayout {
         anchors.left: root.left
         anchors.right: root.right
-        anchors.leftMargin: 15
         anchors.rightMargin: 10
+        anchors.leftMargin: 10
 
         spacing: 0
 
index fe0bcafe6a28bd53869935825631fa59f13f4dcf..7cfc59edccf152b916eb823a395e1cc112602802 100644 (file)
@@ -3,6 +3,7 @@ import QtQuick 2.15
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
 import Style 1.0
+import QtGraphicalEffects 1.15
 import com.nextcloud.desktopclient 1.0
 
 RowLayout {
@@ -19,19 +20,66 @@ RowLayout {
     signal dismissButtonClicked()
     signal shareButtonClicked()
 
-    spacing: 10
-
-    Image {
-        id: activityIcon
+    spacing: Style.trayHorizontalMargin
 
+    Item {
         Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
-        Layout.preferredWidth: 32
-        Layout.preferredHeight: 32
+        Layout.preferredWidth: Style.trayListItemIconSize
+        Layout.preferredHeight: Style.trayListItemIconSize
 
-        verticalAlignment: Qt.AlignCenter
-        source: icon
-        sourceSize.height: 64
-        sourceSize.width: 64
+        Loader {
+            id: thumbnailImageLoader
+            anchors.fill: parent
+            active: model.thumbnail !== undefined
+
+            sourceComponent: Item {
+                anchors.fill: parent
+
+                Image {
+                    id: thumbnailImage
+                    width: model.thumbnail.isMimeTypeIcon ? parent.width * 0.85 : parent.width * 0.8
+                    height: model.thumbnail.isMimeTypeIcon ? parent.height * 0.85 : parent.height * 0.8
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.left: parent.left
+                    cache: true
+                    source: model.thumbnail.source
+                    visible: false
+                    sourceSize.height: 64
+                    sourceSize.width: 64
+                }
+
+                Rectangle {
+                    id: mask
+                    color: "white"
+                    radius: 3
+                    anchors.fill: thumbnailImage
+                    visible: false
+                    width: thumbnailImage.paintedWidth
+                    height: thumbnailImage.paintedHeight
+                }
+
+                OpacityMask {
+                    anchors.fill: thumbnailImage
+                    source: thumbnailImage
+                    maskSource: mask
+                    visible: model.thumbnail !== undefined
+                }
+            }
+        }
+
+        Image {
+            id: activityIcon
+            width: model.thumbnail !== undefined ? parent.width * 0.5 : parent.width * 0.85
+            height: model.thumbnail !== undefined ? parent.height * 0.5 : parent.height * 0.85
+            anchors.verticalCenter: if(model.thumbnail === undefined) parent.verticalCenter
+            anchors.left: if(model.thumbnail === undefined) parent.left
+            anchors.right: if(model.thumbnail !== undefined) parent.right
+            anchors.bottom: if(model.thumbnail !== undefined) parent.bottom
+            cache: true
+            source: icon
+            sourceSize.height: 64
+            sourceSize.width: 64
+        }
     }
 
     Column {
index 7bf9a2d5520e68e9f810cc09032f6764ba8e4516..83a38cf4ff683fed9fbeefe1a772f610fafe8128 100644 (file)
@@ -11,7 +11,7 @@ RowLayout {
 
     property alias model: syncStatus
 
-    spacing: 0
+    spacing: Style.trayHorizontalMargin
 
     NC.SyncStatusSummary {
         id: syncStatus
@@ -19,15 +19,18 @@ RowLayout {
 
     Image {
         id: syncIcon
+        Layout.preferredWidth: Style.trayListItemIconSize * 0.85
+        Layout.preferredHeight: Style.trayListItemIconSize * 0.85
 
         Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
         Layout.topMargin: 16
+        Layout.rightMargin: Style.trayListItemIconSize * 0.15
         Layout.bottomMargin: 16
-        Layout.leftMargin: 16
+        Layout.leftMargin: Style.trayHorizontalMargin
 
         source: syncStatus.syncIcon
-        sourceSize.width: 32
-        sourceSize.height: 32
+        sourceSize.width: 64
+        sourceSize.height: 64
         rotation: syncStatus.syncing ? 0 : 0
     }
  
@@ -45,8 +48,7 @@ RowLayout {
 
         Layout.alignment: Qt.AlignVCenter
         Layout.topMargin: 8
-        Layout.rightMargin: 16
-        Layout.leftMargin: 10
+        Layout.rightMargin: Style.trayHorizontalMargin
         Layout.bottomMargin: 8
         Layout.fillWidth: true
         Layout.fillHeight: true
@@ -65,7 +67,7 @@ RowLayout {
         Loader {
             Layout.fillWidth: true
 
-            active: syncStatus.syncing;
+            active: syncStatus.syncing
             visible: syncStatus.syncing
             
             sourceComponent: ProgressBar {
index 7ca1a99c2ad4866de846b9f21194381959c45188..df99c66308710600825409022967e2041a763e77 100644 (file)
@@ -13,15 +13,15 @@ TextField {
 
     readonly property color textFieldIconsColor: Style.menuBorder
 
-    readonly property int textFieldIconsOffset: 10
+    readonly property int textFieldIconsOffset: Style.trayHorizontalMargin
 
     readonly property double textFieldIconsScaleFactor: 0.6
 
-    readonly property int textFieldHorizontalPaddingOffset: 14
+    readonly property int textFieldHorizontalPaddingOffset: Style.trayHorizontalMargin
 
     signal clearText()
 
-    leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset
+    leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset - 1
     rightPadding: trayWindowUnifiedSearchTextFieldClearTextButton.width + trayWindowUnifiedSearchTextFieldClearTextButton.anchors.rightMargin + textFieldHorizontalPaddingOffset
 
     placeholderText: qsTr("Search files, messages, events …")
@@ -36,6 +36,9 @@ TextField {
 
     Image {
         id: trayWindowUnifiedSearchTextFieldSearchIcon
+        width: Style.trayListItemIconSize - anchors.leftMargin
+        fillMode: Image.PreserveAspectFit
+        horizontalAlignment: Image.AlignLeft
 
         anchors {
             left: parent.left
index 0241ed28e73a9a470c7f9cf9b3329383dcd196d6..69daa19dfe9b590f275e23a2326d5d0c2e329777 100644 (file)
@@ -39,7 +39,7 @@ RowLayout {
             id: unifiedSearchResultThumbnail
             visible: false
             asynchronous: true
-            source: "image://unified-search-result-icon/" + icons
+            source: "image://tray-image-provider/" + icons
             cache: true
             sourceSize.width: imageData.width
             sourceSize.height: imageData.height
index a17b3a0fb06f8cad2aa5dc5e2fe05d454034325c..a6f97a6804114b84f37e2e78ddec07542d2c7c62 100644 (file)
@@ -22,8 +22,8 @@ Window {
     color:      "transparent"\r
     flags:      Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint\r
 \r
-\r
     property int fileActivityDialogObjectId: -1\r
+\r
     readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth\r
 \r
     function openFileActivityDialog(objectName, objectId) {\r
@@ -345,7 +345,7 @@ Window {
                         Image {\r
                             id: currentAccountAvatar\r
 \r
-                            Layout.leftMargin: 8\r
+                            Layout.leftMargin: Style.trayHorizontalMargin\r
                             verticalAlignment: Qt.AlignCenter\r
                             cache: false\r
                             source: UserModel.currentUser.avatar != "" ? UserModel.currentUser.avatar : "image://avatars/fallbackWhite"\r
@@ -601,9 +601,9 @@ Window {
                 left: trayWindowBackground.left\r
                 right: trayWindowBackground.right\r
 \r
-                margins: {\r
-                    top: 10\r
-                }\r
+                topMargin: Style.trayHorizontalMargin + controlRoot.padding\r
+                leftMargin: Style.trayHorizontalMargin + controlRoot.padding\r
+                rightMargin: Style.trayHorizontalMargin + controlRoot.padding\r
             }\r
 \r
             text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm\r
@@ -623,7 +623,7 @@ Window {
             anchors.top: trayWindowUnifiedSearchInputContainer.bottom\r
             anchors.left: trayWindowBackground.left\r
             anchors.right: trayWindowBackground.right\r
-            anchors.margins: 10\r
+            anchors.margins: Style.trayHorizontalMargin\r
         }\r
 \r
         UnifiedSearchResultNothingFound {\r
@@ -632,7 +632,7 @@ Window {
             anchors.top: trayWindowUnifiedSearchInputContainer.bottom\r
             anchors.left: trayWindowBackground.left\r
             anchors.right: trayWindowBackground.right\r
-            anchors.topMargin: 10\r
+            anchors.topMargin: Style.trayHorizontalMargin\r
 \r
             text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm\r
 \r
index 16f1f1c6d7b500a67ecb791c5f631489505abc1a..8a077fc283ae86a9197fed16828430e6c0baeede 100644 (file)
@@ -15,6 +15,7 @@
 #include <QtCore>
 
 #include "activitydata.h"
+#include "folderman.h"
 
 
 namespace OCC {
@@ -44,4 +45,99 @@ ActivityLink ActivityLink::createFomJsonObject(const QJsonObject &obj)
 
     return activityLink;
 }
+
+OCC::Activity Activity::fromActivityJson(const QJsonObject json, const AccountPtr account)
+{
+    const auto activityUser = json.value(QStringLiteral("user")).toString();
+
+    Activity activity;
+    activity._type = Activity::ActivityType;
+    activity._objectType = json.value(QStringLiteral("object_type")).toString();
+    activity._objectId = json.value(QStringLiteral("object_id")).toInt();
+    activity._objectName = json.value(QStringLiteral("object_name")).toString();
+    activity._id = json.value(QStringLiteral("activity_id")).toInt();
+    activity._fileAction = json.value(QStringLiteral("type")).toString();
+    activity._accName = account->displayName();
+    activity._subject = json.value(QStringLiteral("subject")).toString();
+    activity._message = json.value(QStringLiteral("message")).toString();
+    activity._file = json.value(QStringLiteral("object_name")).toString();
+    activity._link = QUrl(json.value(QStringLiteral("link")).toString());
+    activity._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
+    activity._icon = json.value(QStringLiteral("icon")).toString();
+    activity._isCurrentUserFileActivity = activity._objectType == QStringLiteral("files") && activityUser == account->davUser();
+
+    auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
+
+    if(richSubjectData.size() > 1) {
+        activity._subjectRich = richSubjectData[0].toString();
+        auto parameters = richSubjectData[1].toObject();
+        const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
+        const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
+
+        for (auto i = parameters.begin(); i != parameters.end(); ++i) {
+            const auto parameterJsonObject = i.value().toObject();
+
+            activity._subjectRichParameters[i.key()] = Activity::RichSubjectParameter  {
+                parameterJsonObject.value(QStringLiteral("type")).toString(),
+                parameterJsonObject.value(QStringLiteral("id")).toString(),
+                parameterJsonObject.value(QStringLiteral("name")).toString(),
+                parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
+                parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
+            };
+        }
+
+        auto displayString = activity._subjectRich;
+        auto subjectRichParameterMatch = subjectRichParameterRe.globalMatch(displayString);
+
+        while (subjectRichParameterMatch.hasNext()) {
+            const auto match = subjectRichParameterMatch.next();
+            auto word = match.captured(1);
+            word.remove(subjectRichParameterBracesRe);
+
+            Q_ASSERT(activity._subjectRichParameters.contains(word));
+            displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].name);
+        }
+
+        activity._subjectDisplay = displayString;
+    }
+
+    const auto previewsData = json.value(QStringLiteral("previews")).toArray();
+
+    for(const auto preview : previewsData) {
+        const auto jsonPreviewData = preview.toObject();
+
+        PreviewData data;
+        data._link = jsonPreviewData.value(QStringLiteral("link")).toString();
+        data._mimeType = jsonPreviewData.value(QStringLiteral("mimeType")).toString();
+        data._fileId = jsonPreviewData.value(QStringLiteral("fileId")).toInt();
+        data._view = jsonPreviewData.value(QStringLiteral("view")).toString();
+        data._filename = jsonPreviewData.value(QStringLiteral("filename")).toString();
+
+        if(data._mimeType.contains(QStringLiteral("text/"))) {
+            data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg");
+            data._isMimeTypeIcon = true;
+        } else if (data._mimeType.contains(QStringLiteral("/pdf"))) {
+            data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg");
+            data._isMimeTypeIcon = true;
+        } else {
+            data._source = jsonPreviewData.value(QStringLiteral("source")).toString();
+            data._isMimeTypeIcon = jsonPreviewData.value(QStringLiteral("isMimeTypeIcon")).toBool();
+        }
+
+        activity._previews.append(data);
+    }
+
+    if(!previewsData.isEmpty()) {
+        if(activity._icon.contains(QStringLiteral("add-color.svg"))) {
+            activity._icon = "qrc:///client/theme/colored/add-bordered.svg";
+        } else if(activity._icon.contains(QStringLiteral("delete-color.svg"))) {
+            activity._icon = "qrc:///client/theme/colored/delete-bordered.svg";
+        } else if(activity._icon.contains(QStringLiteral("change.svg"))) {
+            activity._icon = "qrc:///client/theme/colored/change-bordered.svg";
+        }
+    }
+
+    return activity;
+}
+
 }
index 49ffd5f3b903298cb745f0d455c505731da02940..20b278326e7bcb140eba8a7f11f8eed85a2b7ffc 100644 (file)
 #include <QIcon>
 #include <QJsonObject>
 
+#include "syncfileitem.h"
+#include "folder.h"
+#include "account.h"
+
 namespace OCC {
 /**
  * @brief The ActivityLink class describes actions of an activity
@@ -49,6 +53,32 @@ public:
     bool _primary;
 };
 
+/**
+ * @brief The PreviewData class describes the data about a file's preview.
+ */
+
+class PreviewData
+{
+    Q_GADGET
+
+    Q_PROPERTY(QString source MEMBER _source)
+    Q_PROPERTY(QString link MEMBER _link)
+    Q_PROPERTY(QString mimeType MEMBER _mimeType)
+    Q_PROPERTY(int fileId MEMBER _fileId)
+    Q_PROPERTY(QString view MEMBER _view)
+    Q_PROPERTY(bool isMimeTypeIcon MEMBER _isMimeTypeIcon)
+    Q_PROPERTY(QString filename MEMBER _filename)
+
+public:
+    QString _source;
+    QString _link;
+    QString _mimeType;
+    int _fileId;
+    QString _view;
+    bool _isMimeTypeIcon;
+    QString _filename;
+};
+
 /* ==================================================================== */
 /**
  * @brief Activity Structure
@@ -69,6 +99,8 @@ public:
         SyncFileItemType
     };
 
+    static Activity fromActivityJson(const QJsonObject json, const AccountPtr account);
+
     struct RichSubjectParameter {
         QString type;    // Required
         QString id;      // Required
@@ -97,6 +129,7 @@ public:
     QString _accName;
     QString _icon;
     bool _isCurrentUserFileActivity = false;
+    QVector<PreviewData> _previews;
 
     // Stores information about the error
     int _status;
@@ -127,5 +160,6 @@ using ActivityList = QList<Activity>;
 
 Q_DECLARE_METATYPE(OCC::Activity::Type)
 Q_DECLARE_METATYPE(OCC::ActivityLink)
+Q_DECLARE_METATYPE(OCC::PreviewData)
 
 #endif // ACTIVITYDATA_H
index a67f8aa94a852d826f625bc040346fc2b6408fed..8cad1589ea76993421dbb37f4a9ef25ee959d556 100644 (file)
@@ -74,6 +74,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
     roles[DisplayActions] = "displayActions";
     roles[ShareableRole] = "isShareable";
     roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
+    roles[ThumbnailRole] = "thumbnail";
     return roles;
 }
 
@@ -175,6 +176,18 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         return displayPath == "." || displayPath == "/" ? QString() : displayPath;
     };
 
+    const auto generatePreviewMap = [](const PreviewData &preview) {
+        return(QVariantMap {
+            {QStringLiteral("source"), QStringLiteral("image://tray-image-provider/").append(preview._source)},
+               {QStringLiteral("link"), preview._link},
+               {QStringLiteral("mimeType"), preview._mimeType},
+               {QStringLiteral("fileId"), preview._fileId},
+               {QStringLiteral("view"), preview._view},
+               {QStringLiteral("isMimeTypeIcon"), preview._isMimeTypeIcon},
+               {QStringLiteral("filename"), preview._filename},
+        });
+    };
+
     switch (role) {
     case DisplayPathRole:
         return getDisplayPath();
@@ -220,11 +233,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
             } else {
                 // File sync successful
                 if (a._fileAction == "file_created") {
-                    return "qrc:///client/theme/colored/add.svg";
+                    return a._previews.empty() ? "qrc:///client/theme/colored/add.svg"
+                                               : "qrc:///client/theme/colored/add-bordered.svg";
                 } else if (a._fileAction == "file_deleted") {
-                    return "qrc:///client/theme/colored/delete.svg";
+                    return a._previews.empty() ? "qrc:///client/theme/colored/delete.svg"
+                                               : "qrc:///client/theme/colored/delete-bordered.svg";
                 } else {
-                    return "qrc:///client/theme/change.svg";
+                    return a._previews.empty() ? "qrc:///client/theme/change.svg"
+                                               : "qrc:///client/theme/colored/change-bordered.svg";
                 }
             }
         } else {
@@ -286,6 +302,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
     case IsCurrentUserFileActivityRole:
         return a._isCurrentUserFileActivity;
+    case ThumbnailRole: {
+        if(a._previews.empty()) {
+            return {};
+        }
+
+        const auto preview = a._previews[0];
+        return(generatePreviewMap(preview));
+    }
     default:
         return QVariant();
     }
@@ -324,6 +348,7 @@ void ActivityListModel::startFetchJob()
         this, &ActivityListModel::activitiesReceived);
 
     QUrlQuery params;
+    params.addQueryItem(QLatin1String("previews"), QLatin1String("true"));
     params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
     params.addQueryItem(QLatin1String("limit"), QString::number(50));
     job->addQueryParams(params);
@@ -348,80 +373,17 @@ int ActivityListModel::currentItem() const
     return _currentItem;
 }
 
-void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
+void ActivityListModel::ingestActivities(const QJsonArray &activities)
 {
-    auto activities = json.object().value("ocs").toObject().value("data").toArray();
-
     ActivityList list;
-    auto ast = _accountState;
-    if (!ast) {
-        return;
-    }
-
-    if (activities.size() == 0) {
-        _doneFetching = true;
-    }
-
-    _currentlyFetching = false;
 
     QDateTime oldestDate = QDateTime::currentDateTime();
     oldestDate = oldestDate.addDays(_maxActivitiesDays * -1);
 
-    foreach (auto activ, activities) {
-        auto json = activ.toObject();
-
-        Activity a;
-        const auto activityUser = json.value(QStringLiteral("user")).toString();
-        a._type = Activity::ActivityType;
-        a._objectType = json.value(QStringLiteral("object_type")).toString();
-        a._objectId = json.value(QStringLiteral("object_id")).toInt();
-        a._objectName = json.value(QStringLiteral("object_name")).toString();
-        a._accName = ast->account()->displayName();
-        a._id = json.value(QStringLiteral("activity_id")).toInt();
-        a._fileAction = json.value(QStringLiteral("type")).toString();
-        a._subject = json.value(QStringLiteral("subject")).toString();
-        a._message = json.value(QStringLiteral("message")).toString();
-        a._file = json.value(QStringLiteral("object_name")).toString();
-        a._link = QUrl(json.value(QStringLiteral("link")).toString());
-        a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
-        a._icon = json.value(QStringLiteral("icon")).toString();
-        a._isCurrentUserFileActivity = a._objectType == QStringLiteral("files") && activityUser == ast->account()->davUser();
-
-        auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
-
-        if(richSubjectData.size() > 1) {
-            a._subjectRich = richSubjectData[0].toString();
-            auto parameters = richSubjectData[1].toObject();
-            const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
-            const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
-
-            for (auto i = parameters.begin(); i != parameters.end(); ++i) {
-                const auto parameterJsonObject = i.value().toObject();
-                const Activity::RichSubjectParameter parameter = {
-                    parameterJsonObject.value(QStringLiteral("type")).toString(),
-                    parameterJsonObject.value(QStringLiteral("id")).toString(),
-                    parameterJsonObject.value(QStringLiteral("name")).toString(),
-                    parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
-                    parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
-                };
-
-                a._subjectRichParameters[i.key()] = parameter;
-            }
-
-            auto displayString = a._subjectRich;
-            auto i = subjectRichParameterRe.globalMatch(displayString);
+    for (const auto &activ : activities) {
+        const auto json = activ.toObject();
 
-            while (i.hasNext()) {
-                const auto match = i.next();
-                auto word = match.captured(1);
-                word.remove(subjectRichParameterBracesRe);
-
-                Q_ASSERT(a._subjectRichParameters.contains(word));
-                displayString = displayString.replace(match.captured(1), a._subjectRichParameters[word].name);
-            }
-
-            a._subjectDisplay = displayString;
-        }
+        const auto a = Activity::fromActivityJson(json, _accountState->account());
 
         list.append(a);
         _currentItem = list.last()._id;
@@ -436,6 +398,23 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
     }
 
     _activityLists.append(list);
+}
+
+void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
+{
+    const auto activities = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray();
+
+    if (!_accountState) {
+        return;
+    }
+
+    if (activities.empty()) {
+        _doneFetching = true;
+    }
+
+    _currentlyFetching = false;
+
+    ingestActivities(activities);
 
     combineActivityLists();
 
index 126ecc28ebe8f459fd73386b55c4ce29d2c383e7..3a688a636e7bdebfbfb981ee92d7a921041150c6 100644 (file)
@@ -66,6 +66,7 @@ public:
         DisplayActions,
         ShareableRole,
         IsCurrentUserFileActivityRole,
+        ThumbnailRole,
     };
     Q_ENUM(DataRole)
 
@@ -136,6 +137,8 @@ private:
     void combineActivityLists();
     bool canFetchActivities() const;
 
+    void ingestActivities(const QJsonArray &activities);
+
     ActivityList _activityLists;
     ActivityList _syncFileItemLists;
     ActivityList _notificationLists;
diff --git a/src/gui/tray/asyncimageresponse.cpp b/src/gui/tray/asyncimageresponse.cpp
new file mode 100644 (file)
index 0000000..e484ab0
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include <QIcon>
+#include <QPainter>
+#include <QSvgRenderer>
+
+#include "asyncimageresponse.h"
+#include "usermodel.h"
+
+AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize)
+{
+    if (id.isEmpty()) {
+        setImageAndEmitFinished();
+        return;
+    }
+
+    _imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts);
+    _requestedImageSize = requestedSize;
+
+    if (_imagePaths.isEmpty()) {
+        setImageAndEmitFinished();
+    } else {
+        processNextImage();
+    }
+}
+
+void AsyncImageResponse::setImageAndEmitFinished(const QImage &image)
+{
+    _image = image;
+    emit finished();
+}
+
+QQuickTextureFactory* AsyncImageResponse::textureFactory() const
+{
+    return QQuickTextureFactory::textureFactoryForImage(_image);
+}
+
+void AsyncImageResponse::processNextImage()
+{
+    if (_index < 0 || _index >= _imagePaths.size()) {
+        setImageAndEmitFinished();
+        return;
+    }
+
+    if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
+        setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
+        return;
+    }
+
+    const auto currentUser = OCC::UserModel::instance()->currentUser();
+    if (currentUser && currentUser->account()) {
+        const QUrl iconUrl(_imagePaths.at(_index));
+        if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) {
+            // fetch the remote resource
+            const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
+            connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
+            ++_index;
+            return;
+        }
+    }
+
+    setImageAndEmitFinished();
+}
+
+void AsyncImageResponse::slotProcessNetworkReply()
+{
+    const auto reply = qobject_cast<QNetworkReply *>(sender());
+    if (!reply) {
+        setImageAndEmitFinished();
+        return;
+    }
+
+    const QByteArray imageData = reply->readAll();
+    // server returns "[]" for some some file previews (have no idea why), so, we use another image
+    // from the list if available
+    if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) {
+        processNextImage();
+    } else {
+        if (imageData.startsWith(QByteArrayLiteral("<svg"))) {
+            // SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer
+            QSvgRenderer svgRenderer;
+            if (svgRenderer.load(imageData)) {
+                QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32);
+                scaledSvg.fill("transparent");
+                QPainter painterForSvg(&scaledSvg);
+                svgRenderer.render(&painterForSvg);
+                setImageAndEmitFinished(scaledSvg);
+                return;
+            } else {
+                processNextImage();
+            }
+        } else {
+            setImageAndEmitFinished(QImage::fromData(imageData));
+        }
+    }
+}
+
diff --git a/src/gui/tray/asyncimageresponse.h b/src/gui/tray/asyncimageresponse.h
new file mode 100644 (file)
index 0000000..b7394ac
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QImage>
+#include <QQuickImageProvider>
+
+class AsyncImageResponse : public QQuickImageResponse
+{
+public:
+    AsyncImageResponse(const QString &id, const QSize &requestedSize);
+    void setImageAndEmitFinished(const QImage &image = {});
+    QQuickTextureFactory *textureFactory() const override;
+
+private:
+    void processNextImage();
+
+private slots:
+    void slotProcessNetworkReply();
+
+    QImage _image;
+    QStringList _imagePaths;
+    QSize _requestedImageSize;
+    int _index = 0;
+};
diff --git a/src/gui/tray/trayimageprovider.cpp b/src/gui/tray/trayimageprovider.cpp
new file mode 100644 (file)
index 0000000..b9a98b4
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "trayimageprovider.h"
+#include "asyncimageresponse.h"
+
+namespace OCC {
+
+QQuickImageResponse *TrayImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
+{
+    return new AsyncImageResponse(id, requestedSize);
+}
+
+}
diff --git a/src/gui/tray/trayimageprovider.h b/src/gui/tray/trayimageprovider.h
new file mode 100644 (file)
index 0000000..fc2076a
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QtCore>
+#include <QQuickImageProvider>
+
+namespace OCC {
+
+/**
+ * @brief The TrayImageProvider
+ * @ingroup gui
+ * Allows to fetch icon from the server or used a local resource
+ */
+
+class TrayImageProvider : public QQuickAsyncImageProvider
+{
+public:
+    QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
+};
+}
diff --git a/src/gui/tray/unifiedsearchresultimageprovider.cpp b/src/gui/tray/unifiedsearchresultimageprovider.cpp
deleted file mode 100644 (file)
index 97a57f5..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#include "unifiedsearchresultimageprovider.h"
-
-#include "usermodel.h"
-
-#include <QImage>
-#include <QPainter>
-#include <QSvgRenderer>
-
-namespace {
-class AsyncImageResponse : public QQuickImageResponse
-{
-public:
-    AsyncImageResponse(const QString &id, const QSize &requestedSize)
-    {
-        if (id.isEmpty()) {
-            setImageAndEmitFinished();
-            return;
-        }
-
-        _imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts);
-        _requestedImageSize = requestedSize;
-
-        if (_imagePaths.isEmpty()) {
-            setImageAndEmitFinished();
-        } else {
-            processNextImage();
-        }
-    }
-
-    void setImageAndEmitFinished(const QImage &image = {})
-    {
-        _image = image;
-        emit finished();
-    }
-
-    QQuickTextureFactory *textureFactory() const override
-    {
-        return QQuickTextureFactory::textureFactoryForImage(_image);
-    }
-
-private:
-    void processNextImage()
-    {
-        if (_index < 0 || _index >= _imagePaths.size()) {
-            setImageAndEmitFinished();
-            return;
-        }
-
-        if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
-            setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
-            return;
-        }
-
-        const auto currentUser = OCC::UserModel::instance()->currentUser();
-        if (currentUser && currentUser->account()) {
-            const QUrl iconUrl(_imagePaths.at(_index));
-            if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) {
-                // fetch the remote resource
-                const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
-                connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
-                ++_index;
-                return;
-            }
-        }
-
-        setImageAndEmitFinished();
-    }
-
-private slots:
-    void slotProcessNetworkReply()
-    {
-        const auto reply = qobject_cast<QNetworkReply *>(sender());
-        if (!reply) {
-            setImageAndEmitFinished();
-            return;
-        }
-
-        const QByteArray imageData = reply->readAll();
-        // server returns "[]" for some some file previews (have no idea why), so, we use another image
-        // from the list if available
-        if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) {
-            processNextImage();
-        } else {
-            if (imageData.startsWith(QByteArrayLiteral("<svg"))) {
-                // SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer
-                QSvgRenderer svgRenderer;
-                if (svgRenderer.load(imageData)) {
-                    QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32);
-                    scaledSvg.fill("transparent");
-                    QPainter painterForSvg(&scaledSvg);
-                    svgRenderer.render(&painterForSvg);
-                    setImageAndEmitFinished(scaledSvg);
-                    return;
-                } else {
-                    processNextImage();
-                }
-            } else {
-                setImageAndEmitFinished(QImage::fromData(imageData));
-            }
-        }
-    }
-
-    QImage _image;
-    QStringList _imagePaths;
-    QSize _requestedImageSize;
-    int _index = 0;
-};
-}
-
-namespace OCC {
-
-QQuickImageResponse *UnifiedSearchResultImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
-{
-    return new AsyncImageResponse(id, requestedSize);
-}
-
-}
diff --git a/src/gui/tray/unifiedsearchresultimageprovider.h b/src/gui/tray/unifiedsearchresultimageprovider.h
deleted file mode 100644 (file)
index 0e35c9b..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#pragma once
-
-#include <QtCore>
-#include <QQuickImageProvider>
-
-namespace OCC {
-
-/**
- * @brief The UnifiedSearchResultImageProvider
- * @ingroup gui
- * Allows to fetch Unified Search result icon from the server or used a local resource
- */
-
-class UnifiedSearchResultImageProvider : public QQuickAsyncImageProvider
-{
-public:
-    QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
-};
-}
index 17eb51645526a2cdfa29b193d7e50292570ac58b..487d995cb9b168053449d4fe9e763dad74b38550 100644 (file)
@@ -16,6 +16,7 @@
 #include "tray/notificationcache.h"
 #include "tray/unifiedsearchresultslistmodel.h"
 #include "userstatusconnector.h"
+#include "thumbnailjob.h"
 
 #include <QDesktopServices>
 #include <QIcon>
@@ -499,50 +500,88 @@ bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const
 
 void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item)
 {
+    const auto fileActionFromInstruction = [](const int instruction) {
+        if (instruction == CSYNC_INSTRUCTION_REMOVE) {
+            return QStringLiteral("file_deleted");
+        } else if (instruction == CSYNC_INSTRUCTION_NEW) {
+            return QStringLiteral("file_created");
+        } else if (instruction == CSYNC_INSTRUCTION_RENAME) {
+            return QStringLiteral("file_renamed");
+        } else {
+            return QStringLiteral("file_changed");
+        }
+    };
+
+    const auto messageFromFileAction = [](const QString &fileAction, const QString &fileName) {
+        if (fileAction == QStringLiteral("file_renamed")) {
+            return QObject::tr("You renamed %1").arg(fileName);
+        } else if (fileAction == QStringLiteral("file_deleted")) {
+            return QObject:: tr("You deleted %1").arg(fileName);
+        } else if (fileAction == QStringLiteral("file_created")) {
+            return QObject::tr("You created %1").arg(fileName);
+        } else {
+            return QObject::tr("You changed %1").arg(fileName);
+        }
+    };
+
     Activity activity;
     activity._type = Activity::SyncFileItemType; //client activity
     activity._status = item->_status;
     activity._dateTime = QDateTime::currentDateTime();
     activity._message = item->_originalFile;
-    activity._link = folder->accountState()->account()->url();
-    activity._accName = folder->accountState()->account()->displayName();
+    activity._link = account()->url();
+    activity._accName = account()->displayName();
     activity._file = item->_file;
     activity._folder = folder->alias();
     activity._fileAction = "";
-    activity._objectId = item->_fileId.toInt();
-    activity._objectName = item->_file;
 
     const auto fileName = QFileInfo(item->_originalFile).fileName();
 
-    if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
-        activity._fileAction = "file_deleted";
-    } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
-        activity._fileAction = "file_created";
-    } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
-        activity._fileAction = "file_renamed";
-        activity._renamedFile = item->_renameTarget;
-    } else {
-        activity._fileAction = "file_changed";
-    }
+    activity._fileAction = fileActionFromInstruction(item->_instruction);
 
     if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
         qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
 
         if (item->_direction != SyncFileItem::Up) {
-            activity._message = tr("Synced %1").arg(fileName);
-        } else if (activity._fileAction == "file_renamed") {
-            activity._message = tr("You renamed %1").arg(fileName);
-        } else if (activity._fileAction == "file_deleted") {
-            activity._message = tr("You deleted %1").arg(fileName);
-        } else if (activity._fileAction == "file_created") {
-            activity._message = tr("You created %1").arg(fileName);
+            activity._message = QObject::tr("Synced %1").arg(fileName);
         } else {
-            activity._message = tr("You changed %1").arg(fileName);
+            activity._message = messageFromFileAction(activity._fileAction, fileName);
+        }
+
+        if(activity._fileAction != "file_deleted") {
+            auto remotePath = folder->remotePath();
+            remotePath.append(activity._fileAction == "file_renamed" ? item->_renameTarget : activity._file);
+
+            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(item->_file, account());
+            if (!localFiles.isEmpty()) {
+                const QMimeType mimeType = _mimeDb.mimeTypeForFile(QFileInfo(localFiles.constFirst()));
+
+                // Set the preview data, though for now we can skip setting file ID, link, and view
+                PreviewData preview;
+                preview._mimeType = mimeType.name();
+                preview._filename = fileName;
+
+                if(item->isDirectory()) {
+                    preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/folder.svg");
+                    preview._isMimeTypeIcon = true;
+                } else if(mimeType.isValid() && mimeType.inherits("text/plain")) {
+                    preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg");
+                    preview._isMimeTypeIcon = true;
+                } else if (mimeType.isValid() && mimeType.inherits("application/pdf")) {
+                    preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg");
+                    preview._isMimeTypeIcon = true;
+                } else {
+                    preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/files/api/v1/thumbnail/150/150/") + remotePath;
+                    preview._isMimeTypeIcon = false;
+                }
+                activity._previews.append(preview);
+            }
         }
 
         _activityModel->addSyncFileItemToActivityList(activity);
     } else {
         qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
+
         activity._subject = item->_errorString;
 
         if (item->_status == SyncFileItem::Status::FileIgnored) {
index 36beaa0294a67fd0b741db48763b1609db3de614..8844ef2919330ed65ad10ba55d83426fa84ed656 100644 (file)
@@ -134,6 +134,7 @@ private:
 
     QElapsedTimer _guiLogTimer;
     NotificationCache _notificationCache;
+    QMimeDatabase _mimeDb;
 
     // number of currently running notification requests. If non zero,
     // no query for notifications is started.
index 011ad55805ac79fe7a4ad378bb5297d80ea59e48..ccecd5fce23f9ec6f84b55922419d8be8b0155f9 100644 (file)
@@ -62,6 +62,7 @@ nextcloud_add_test(NotificationCache)
 nextcloud_add_test(SetUserStatusDialog)
 nextcloud_add_test(UnifiedSearchListmodel)
 nextcloud_add_test(ActivityListModel)
+nextcloud_add_test(ActivityData)
 
 if( UNIX AND NOT APPLE )
     nextcloud_add_test(InotifyWatcher)
diff --git a/test/testactivitydata.cpp b/test/testactivitydata.cpp
new file mode 100644 (file)
index 0000000..28def53
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "gui/tray/activitydata.h"
+#include "account.h"
+#include "accountstate.h"
+#include "configfile.h"
+#include "syncenginetestutils.h"
+#include "syncfileitem.h"
+#include "folder.h"
+#include "folderman.h"
+#include "testhelper.h"
+
+#include <QTest>
+
+class TestActivityData : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestActivityData() = default;
+
+    void createJsonSpecificFormatData(QString fileFormat, QString mimeType)
+    {
+        const auto objectType = QStringLiteral("files");
+        const auto subject = QStringLiteral("You created path/test.").append(fileFormat);
+        const auto path = QStringLiteral("path/test.").append(fileFormat);
+        const auto fileName = QStringLiteral("test.").append(fileFormat);
+        const auto activityType = QStringLiteral("file");
+        const auto activityId = 90000;
+        const auto message = QStringLiteral();
+        const auto objectName = QStringLiteral("test.").append(fileFormat);
+        const auto link = account->url().toString().append(QStringLiteral("/f/")).append(activityId);
+        const auto datetime = QDateTime::currentDateTime().toString(Qt::ISODate);
+        const auto icon = account->url().toString().append(QStringLiteral("/apps/files/img/add-color.svg"));
+
+        const QJsonObject richStringData({
+            {QStringLiteral("type"), activityType},
+            {QStringLiteral("id"), activityId},
+            {QStringLiteral("link"),  link},
+            {QStringLiteral("name"), fileName},
+            {QStringLiteral("path"), objectName}
+        });
+
+        const auto subjectRichString = QStringLiteral("You created {file1}");
+        const auto subjectRichObj = QJsonObject({{QStringLiteral("file1"), richStringData}});
+        const auto subjectRichData = QJsonArray({subjectRichString, subjectRichObj});
+
+        const auto previewUrl = account->url().toString().append(QStringLiteral("/index.php/core/preview.png?file=/")).append(path);
+
+        // Text file previews should be replaced by mimetype icon
+        const QJsonObject previewData({
+            {QStringLiteral("link"), link},
+            {QStringLiteral("mimeType"), mimeType},
+            {QStringLiteral("fileId"), activityId},
+            {QStringLiteral("filename"), fileName},
+            {QStringLiteral("view"), QStringLiteral("files")},
+            {QStringLiteral("source"), previewUrl},
+            {QStringLiteral("isMimeTypeIcon"), false},
+        });
+
+        QJsonObject testData({
+            {QStringLiteral("object_type"), objectType},
+            {QStringLiteral("activity_id"), activityId},
+            {QStringLiteral("type"), activityType},
+            {QStringLiteral("subject"), subject},
+            {QStringLiteral("message"), message},
+            {QStringLiteral("object_name"), objectName},
+            {QStringLiteral("link"), link},
+            {QStringLiteral("datetime"), datetime},
+            {QStringLiteral("icon"), icon},
+            {QStringLiteral("subject_rich"), subjectRichData},
+            {QStringLiteral("previews"), QJsonArray({previewData})},
+        });
+
+        QTest::addRow("data") << testData << fileFormat << mimeType << objectType << subject << path << fileName << activityType << activityId << message << objectName << link << datetime << icon << subjectRichString << subjectRichData << previewUrl;
+    }
+
+    QScopedPointer<FakeQNAM> fakeQnam;
+    OCC::AccountPtr account;
+
+private slots:
+    void initTestCase()
+    {
+        account = OCC::Account::create();
+        account->setCredentials(new FakeCredentials{fakeQnam.data()});
+        account->setUrl(QUrl(("http://example.de")));
+        auto *cred = new HttpCredentialsTest("testuser", "secret");
+        account->setCredentials(cred);
+    }
+
+    void testFromJson_data()
+    {
+        QTest::addColumn<QJsonObject>("activityJsonObject");
+        QTest::addColumn<QString>("fileFormat");
+        QTest::addColumn<QString>("mimeTypeExpected");
+        QTest::addColumn<QString>("objectTypeExpected");
+        QTest::addColumn<QString>("subjectExpected");
+        QTest::addColumn<QString>("pathExpected");
+        QTest::addColumn<QString>("fileNameExpected");
+        QTest::addColumn<QString>("activityTypeExpected");
+        QTest::addColumn<int>("activityIdExpected");
+        QTest::addColumn<QString>("messageExpected");
+        QTest::addColumn<QString>("objectNameExpected");
+        QTest::addColumn<QString>("linkExpected");
+        QTest::addColumn<QString>("datetimeExpected");
+        QTest::addColumn<QString>("iconExpected");
+        QTest::addColumn<QString>("subjectRichStringExpected");
+        QTest::addColumn<QJsonArray>("subjectRichDataExpected");
+        QTest::addColumn<QString>("previewUrlExpected");
+
+        createJsonSpecificFormatData(QStringLiteral("jpg"), QStringLiteral("image/jpg"));
+        createJsonSpecificFormatData(QStringLiteral("txt"), QStringLiteral("text/plain"));
+        createJsonSpecificFormatData(QStringLiteral("pdf"), QStringLiteral("application/pdf"));
+    }
+
+    void testFromJson()
+    {
+        QFETCH(QJsonObject, activityJsonObject);
+        QFETCH(QString, fileFormat);
+        QFETCH(QString, mimeTypeExpected);
+        QFETCH(QString, objectTypeExpected);
+        QFETCH(QString, subjectExpected);
+        QFETCH(QString, pathExpected);
+        QFETCH(QString, fileNameExpected);
+        QFETCH(QString, activityTypeExpected);
+        QFETCH(int, activityIdExpected);
+        QFETCH(QString, messageExpected);
+        QFETCH(QString, objectNameExpected);
+        QFETCH(QString, linkExpected);
+        QFETCH(QString, datetimeExpected);
+        QFETCH(QString, iconExpected);
+        QFETCH(QString, subjectRichStringExpected);
+        QFETCH(QJsonArray, subjectRichDataExpected);
+        QFETCH(QString, previewUrlExpected);
+
+        OCC::Activity activity = OCC::Activity::fromActivityJson(activityJsonObject, account);
+        QCOMPARE(activity._type, OCC::Activity::ActivityType);
+        QCOMPARE(activity._objectType, objectTypeExpected);
+        QCOMPARE(activity._id, activityIdExpected);
+        QCOMPARE(activity._fileAction, activityTypeExpected);
+        QCOMPARE(activity._accName, account->displayName());
+        QCOMPARE(activity._subject, subjectExpected);
+        QCOMPARE(activity._message, messageExpected);
+        QCOMPARE(activity._file, objectNameExpected);
+        QCOMPARE(activity._link, linkExpected);
+        QCOMPARE(activity._dateTime, QDateTime::fromString(datetimeExpected, Qt::ISODate));
+
+        QCOMPARE(activity._subjectRichParameters.count(), 1);
+        QCOMPARE(activity._subjectDisplay, QStringLiteral("You created ").append(fileNameExpected));
+
+        QCOMPARE(activity._previews.count(), 1);
+        // We want the different icon when we have a preview
+        //QCOMPARE(activity._icon, iconExpected);
+
+        if(fileFormat == "txt") {
+            QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg")));
+            QCOMPARE(activity._previews[0]._isMimeTypeIcon, true);
+            QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected);
+        } else if(fileFormat == "pdf") {
+            QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg")));
+            QCOMPARE(activity._previews[0]._isMimeTypeIcon, true);
+        } else {
+            QCOMPARE(activity._previews[0]._source, previewUrlExpected);
+            QCOMPARE(activity._previews[0]._isMimeTypeIcon, false);
+        }
+
+        QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected);
+    }
+};
+
+QTEST_MAIN(TestActivityData)
+#include "testactivitydata.moc"
index f927692f8b9a13fd584fa5b7fccdd5386cef7501..c2d84b95fa5a5559a4445eef64b8fb0afb7b73c8 100644 (file)
         <file>theme/copy.svg</file>
         <file>theme/more.svg</file>
         <file>theme/change.svg</file>
+        <file>theme/colored/change-bordered.svg</file>
         <file>theme/lock-http.svg</file>
         <file>theme/lock-https.svg</file>
         <file>theme/lock-broken.svg</file>
         <file>theme/network.svg</file>
         <file>theme/account.svg</file>
         <file>theme/colored/add.svg</file>
+        <file>theme/colored/add-bordered.svg</file>
         <file>theme/colored/delete.svg</file>
+        <file>theme/colored/delete-bordered.svg</file>
         <file>theme/colored/@APPLICATION_ICON_NAME@-icon.svg</file>
         <file>theme/add.svg</file>
         <file>theme/share.svg</file>
index 1425de189ccb4fe7c2cca467b8421c1893374abc..d25f7c6f4806222db0e385b58f0975816ea522d9 100644 (file)
@@ -29,6 +29,8 @@ QtObject {
     property int trayWindowRadius: 10\r
     property int trayWindowBorderWidth: 1\r
     property int trayWindowHeaderHeight: variableSize(60)\r
+    property int trayHorizontalMargin: 10\r
+    property int trayListItemIconSize: accountAvatarSize\r
 \r
     property int currentAccountButtonWidth: 220\r
     property int currentAccountButtonRadius: 2\r
@@ -58,7 +60,7 @@ QtObject {
     property int roundedButtonBackgroundVerticalMargins: 5\r
     \r
     property int userStatusEmojiSize: 8\r
-    property int userStatusSpacing: 6\r
+    property int userStatusSpacing: trayHorizontalMargin\r
     property int userStatusAnchorsMargin: 2\r
     property int accountServerAnchorsMargin: 10\r
     property int accountLabelsSpacing: 4\r
diff --git a/theme/colored/add-bordered.svg b/theme/colored/add-bordered.svg
new file mode 100644 (file)
index 0000000..76b6284
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   width="16"
+   version="1.1"
+   viewbox="0 0 16 16"
+   id="svg4"
+   sodipodi:docname="add-bordered.svg"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     id="namedview6"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="39.375"
+     inkscape:cx="7.9873016"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1020"
+     inkscape:window-x="1"
+     inkscape:window-y="1111"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     fill="#000000"
+     d="m 9.02,13.98 h -2 V 8.9799996 h -5 V 6.98 h 5 v -5 h 2 v 5 l 5,-0.028 v 2.0279996 h -5 z"
+     id="path2929"
+     style="fill:#000001;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
+  <path
+     fill="#000"
+     d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"
+     id="path2" />
+</svg>
diff --git a/theme/colored/change-bordered.svg b/theme/colored/change-bordered.svg
new file mode 100644 (file)
index 0000000..01b4b71
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   height="16"
+   id="svg6"
+   sodipodi:docname="change-bordered.svg"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="27.84233"
+     inkscape:cx="6.0878526"
+     inkscape:cy="3.9867353"
+     inkscape:window-width="1920"
+     inkscape:window-height="1020"
+     inkscape:window-x="1"
+     inkscape:window-y="1111"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg6" />
+  <path
+     d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
+     id="path2731"
+     style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
+  <path
+     d="M 8,2 C 5.858,2 3.875,3.145 2.804,5 L 4.752,6.125 C 5.423,4.963 6.658,4.25 7.9996,4.25 c 1.1906,0 2.297,0.56157 3,1.5 l -1.5,1.5 h 4.5 v -4.5 l -1.406,1.406 C 11.4646,2.808 9.792,2 8,2 Z"
+     id="path827"
+     style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
+     sodipodi:nodetypes="cccsccccccc" />
+  <path
+     d="m8 2c-2.142 0-4.125 1.145-5.196 3l1.948 1.125c0.671-1.162 1.906-1.875 3.2476-1.875 1.1906 0 2.297 0.56157 3 1.5l-1.5 1.5h4.5v-4.5l-1.406 1.406c-1.129-1.348-2.802-2.1563-4.594-2.1563z"
+     id="path2" />
+  <path
+     d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
+     id="path4" />
+</svg>
diff --git a/theme/colored/delete-bordered.svg b/theme/colored/delete-bordered.svg
new file mode 100644 (file)
index 0000000..4a6d969
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   width="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg4"
+   sodipodi:docname="delete-bordered.svg"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     id="namedview6"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="39.375"
+     inkscape:cx="7.9873016"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1020"
+     inkscape:window-x="1"
+     inkscape:window-y="1111"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
+     fill="#000"
+     id="path849"
+     style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
+  <path
+     d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
+     fill="#000"
+     id="path2" />
+</svg>