Pārlūkot izejas kodu

Add file activity dialog

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
Felix Weilbach 4 gadi atpakaļ
vecāks
revīzija
0c9dce1154

+ 2 - 0
resources.qrc

@@ -12,5 +12,7 @@
         <file>src/gui/tray/ActivityActionButton.qml</file>
         <file>src/gui/tray/ActivityItem.qml</file>
         <file>src/gui/tray/AutoSizingMenu.qml</file>
+        <file>src/gui/tray/ActivityList.qml</file>
+        <file>src/gui/tray/FileActivityDialog.qml</file>
     </qresource>
 </RCC>

+ 2 - 0
src/gui/CMakeLists.txt

@@ -40,6 +40,7 @@ set(client_UI_SRCS
     UserStatusSelectorDialog.qml
     tray/ActivityActionButton.qml
     tray/ActivityItem.qml
+    tray/ActivityList.qml
     tray/Window.qml
     tray/UserLine.qml
     wizard/flow2authwidget.ui
@@ -111,6 +112,7 @@ set(client_SRCS
     remotewipe.cpp
     userstatusselectormodel.cpp
     emojimodel.cpp
+    fileactivitylistmodel.cpp
     tray/ActivityData.cpp
     tray/ActivityListModel.cpp
     tray/UserModel.cpp

+ 3 - 0
src/gui/application.cpp

@@ -349,6 +349,9 @@ Application::Application(int &argc, char **argv)
     connect(FolderMan::instance()->socketApi(), &SocketApi::shareCommandReceived,
         _gui.data(), &ownCloudGui::slotShowShareDialog);
 
+    connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived,
+        Systray::instance(), &Systray::showFileActivityDialog);
+
     // startup procedure.
     connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection);
     _checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec); // check for connection every 32 seconds.

+ 73 - 0
src/gui/fileactivitylistmodel.cpp

@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@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 "fileactivitylistmodel.h"
+#include "folderman.h"
+#include "tray/ActivityListModel.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcFileActivityListModel, "nextcloud.gui.fileactivitylistmodel", QtInfoMsg)
+
+FileActivityListModel::FileActivityListModel(QObject *parent)
+    : ActivityListModel(nullptr, parent)
+{
+    setDisplayActions(false);
+}
+
+void FileActivityListModel::load(AccountState *accountState, const QString &localPath)
+{
+    Q_ASSERT(accountState);
+    if (!accountState || currentlyFetching()) {
+        return;
+    }
+    setAccountState(accountState);
+
+    const auto folder = FolderMan::instance()->folderForPath(localPath);
+    if (!folder) {
+        return;
+    }
+
+    const auto file = folder->fileFromLocalPath(localPath);
+    SyncJournalFileRecord fileRecord;
+    if (!folder->journalDb()->getFileRecord(file, &fileRecord) || !fileRecord.isValid()) {
+        return;
+    }
+
+    _fileId = fileRecord._fileId;
+    slotRefreshActivity();
+}
+
+void FileActivityListModel::startFetchJob()
+{
+    if (!accountState()->isConnected()) {
+        return;
+    }
+    setCurrentlyFetching(true);
+
+    const QString url(QStringLiteral("ocs/v2.php/apps/activity/api/v2/activity/filter"));
+    auto job = new JsonApiJob(accountState()->account(), url, this);
+    QObject::connect(job, &JsonApiJob::jsonReceived,
+        this, &FileActivityListModel::activitiesReceived);
+
+    QUrlQuery params;
+    params.addQueryItem(QStringLiteral("sort"), QStringLiteral("asc"));
+    params.addQueryItem(QStringLiteral("object_type"), "files");
+    params.addQueryItem(QStringLiteral("object_id"), _fileId);
+    job->addQueryParams(params);
+    setDoneFetching(true);
+    setHideOldActivities(true);
+    job->start();
+}
+}

+ 38 - 0
src/gui/fileactivitylistmodel.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@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 "accountstate.h"
+#include "tray/ActivityListModel.h"
+
+namespace OCC {
+
+class FileActivityListModel : public ActivityListModel
+{
+    Q_OBJECT
+
+public:
+    explicit FileActivityListModel(QObject *parent = nullptr);
+
+public slots:
+    void load(AccountState *accountState, const QString &fileId);
+
+protected:
+    void startFetchJob() override;
+
+private:
+    QString _fileId;
+};
+}

+ 6 - 0
src/gui/folder.cpp

@@ -1261,6 +1261,11 @@ void Folder::slotAboutToRemoveAllFiles(SyncFileItem::Direction dir, std::functio
     msgBox->open();
 }
 
+QString Folder::fileFromLocalPath(const QString &localPath) const
+{
+    return localPath.mid(cleanPath().length() + 1);
+}
+
 void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
 {
     settings.setValue(QLatin1String("localPath"), folder.localPath);
@@ -1354,4 +1359,5 @@ QString FolderDefinition::defaultJournalPath(AccountPtr account)
     return SyncJournalDb::makeDbName(localPath, account->url(), targetPath, account->credentials()->user());
 }
 
+
 } // namespace OCC

+ 2 - 0
src/gui/folder.h

@@ -290,6 +290,8 @@ public:
     /** Whether this folder should show selective sync ui */
     bool supportsSelectiveSync() const;
 
+    QString fileFromLocalPath(const QString &localPath) const;
+
 signals:
     void syncStateChange();
     void syncStarted();

+ 7 - 4
src/gui/main.cpp

@@ -24,6 +24,7 @@
 #endif
 
 #include "application.h"
+#include "fileactivitylistmodel.h"
 #include "theme.h"
 #include "common/utility.h"
 #include "cocoainitializer.h"
@@ -59,11 +60,13 @@ int main(int argc, char **argv)
     Q_INIT_RESOURCE(theme);
 
     qmlRegisterType<EmojiModel>("com.nextcloud.desktopclient", 1, 0, "EmojiModel");
+    qmlRegisterType<UserStatusSelectorModel>("com.nextcloud.desktopclient", 1, 0, "UserStatusSelectorModel");
+    qmlRegisterType<OCC::ActivityListModel>("com.nextcloud.desktopclient", 1, 0, "ActivityListModel");
+    qmlRegisterType<OCC::FileActivityListModel>("com.nextcloud.desktopclient", 1, 0, "FileActivityListModel");
+
+    qmlRegisterUncreatableType<OCC::UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
+
     qRegisterMetaTypeStreamOperators<Emoji>();
-    qmlRegisterType<UserStatusSelectorModel>("com.nextcloud.desktopclient", 1, 0,
-        "UserStatusSelectorModel");
-    qmlRegisterUncreatableType<OCC::UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus",
-        "Access to Status enum");
     qRegisterMetaType<OCC::UserStatus>("UserStatus");
 
 

+ 21 - 3
src/gui/socketapi/socketapi.cpp

@@ -494,6 +494,12 @@ void SocketApi::broadcastMessage(const QString &msg, bool doWait)
     }
 }
 
+void SocketApi::processFileActivityRequest(const QString &localFile)
+{
+    const auto fileData = FileData::get(localFile);
+    emit fileActivityCommandReceived(fileData.serverRelativePath, fileData.localPath);
+}
+
 void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage)
 {
     auto theme = Theme::instance();
@@ -578,6 +584,13 @@ void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener
     processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups);
 }
 
+void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *listener)
+{
+    Q_UNUSED(listener);
+
+    processFileActivityRequest(localFile);
+}
+
 void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener)
 {
     processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks);
@@ -976,12 +989,13 @@ void OCC::SocketApi::openPrivateLink(const QString &link)
 
 void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener)
 {
-    static std::array<std::pair<const char *, QString>, 5> strings { {
+    static std::array<std::pair<const char *, QString>, 6> strings { {
         { "SHARE_MENU_TITLE", tr("Share options") },
+        { "FILE_ACTIVITY_MENU_TITLE", tr("Activity") },
         { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() },
         { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") },
         { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") },
-        { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME},
+        { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME },
     } };
     listener->sendMessage(QString("GET_STRINGS:BEGIN"));
     for (const auto& key_value : strings) {
@@ -1118,6 +1132,11 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
         const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
         auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
 
+        const QFileInfo fileInfo(fileData.localPath);
+        if (!fileInfo.isDir()) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:ACTIVITY") + flagString + tr("Activity"));
+        }
+
         DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
         if (editor) {
             //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name());
@@ -1132,7 +1151,6 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
         bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);
         if (isConflict || !isOnTheServer) {
             // Check whether this new file is in a read-only directory
-            QFileInfo fileInfo(fileData.localPath);
             const auto parentDir = fileData.parentFolder();
             const auto parentRecord = parentDir.journalRecord();
             const bool canAddToDir =

+ 3 - 0
src/gui/socketapi/socketapi.h

@@ -64,6 +64,7 @@ public slots:
 
 signals:
     void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage);
+    void fileActivityCommandReceived(const QString &sharePath, const QString &localPath);
 
 private slots:
     void slotNewConnection();
@@ -102,6 +103,7 @@ private:
 
     // opens share dialog, sends reply
     void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage);
+    void processFileActivityRequest(const QString &localFile);
 
     Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener);
     Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener);
@@ -111,6 +113,7 @@ private:
     Q_INVOKABLE void command_SHARE_MENU_TITLE(const QString &argument, SocketListener *listener);
 
     // The context menu actions
+    Q_INVOKABLE void command_ACTIVITY(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener);

+ 1 - 0
src/gui/systray.h

@@ -79,6 +79,7 @@ signals:
     void hideWindow();
     void showWindow();
     void openShareDialog(const QString &sharePath, const QString &localPath);
+    void showFileActivityDialog(const QString &sharePath, const QString &localPath);
 
 public slots:
     void slotNewUserSelected();

+ 42 - 40
src/gui/tray/ActivityItem.qml

@@ -7,9 +7,15 @@ import com.nextcloud.desktopclient 1.0
 
 MouseArea {
     id: activityMouseArea
+
+    readonly property int maxActionButtons: 2
+    property Flickable flickable
+
+    signal fileActivityButtonClicked(string absolutePath)
+
     enabled: (path !== "" || link !== "")
     hoverEnabled: true
-    
+
     Rectangle {
         anchors.fill: parent
         anchors.margins: 2
@@ -106,7 +112,7 @@ MouseArea {
             }
             
             Repeater {
-                model: activityItem.links.length > activityListView.maxActionButtons ? 1 : activityItem.links.length
+                model: activityItem.links.length > maxActionButtons ? 1 : activityItem.links.length
                 
                 ActivityActionButton {
                     id: activityActionButton
@@ -139,6 +145,31 @@ MouseArea {
                 }
                 
             }
+
+            Button {
+                id: shareButton
+                
+                Layout.preferredWidth:  parent.height
+                Layout.fillHeight: true
+                Layout.alignment: Qt.AlignRight
+                flat: true
+                hoverEnabled: true
+                visible: displayActions && (path !== "")
+                display: AbstractButton.IconOnly
+                icon.source: "qrc:///client/theme/share.svg"
+                icon.color: "transparent"
+                background: Rectangle {
+                    color: parent.hovered ? Style.lightHover : "transparent"
+                }
+                ToolTip.visible: hovered
+                ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
+                ToolTip.text: qsTr("Open share dialog")
+                onClicked: Systray.openShareDialog(displayPath, absolutePath)
+                
+                Accessible.role: Accessible.Button
+                Accessible.name: qsTr("Share %1").arg(displayPath)
+                Accessible.onPressAction: shareButton.clicked()
+            }
             
             Button {
                 id: moreActionsButton
@@ -149,7 +180,7 @@ MouseArea {
                 
                 flat: true
                 hoverEnabled: true
-                visible: activityItem.links.length > activityListView.maxActionButtons
+                visible: displayActions && ((path !== "") || (activityItem.links.length > maxActionButtons))
                 display: AbstractButton.IconOnly
                 icon.source: "qrc:///client/theme/more.svg"
                 icon.color: "transparent"
@@ -157,7 +188,7 @@ MouseArea {
                     color: parent.hovered ? Style.lightHover : "transparent"
                 }
                 ToolTip.visible: hovered
-                ToolTip.delay: 1000
+                ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
                 ToolTip.text: qsTr("Show more actions")
                 
                 Accessible.role: Accessible.Button
@@ -167,16 +198,7 @@ MouseArea {
                 onClicked:  moreActionsButtonContextMenu.popup();
                 
                 Connections {
-                    target: trayWindow
-                    function onActiveChanged() {
-                        if (!trayWindow.active) {
-                            moreActionsButtonContextMenu.close();
-                        }
-                    }
-                }
-                
-                Connections {
-                    target: activityListView
+                    target: flickable
                     
                     function onMovementStarted() {
                         moreActionsButtonContextMenu.close();
@@ -199,7 +221,7 @@ MouseArea {
                         // transform model to contain indexed actions with primary action filtered out
                         function actionListToContextMenuList(actionList) {
                             // early out with non-altered data
-                            if (activityItem.links.length <= activityListView.maxActionButtons) {
+                            if (activityItem.links.length <= maxActionButtons) {
                                 return actionList;
                             }
                             
@@ -215,6 +237,11 @@ MouseArea {
                             
                             return reducedActionList;
                         }
+
+                        MenuItem {
+                            text: qsTr("View activity")
+                            onClicked: fileActivityButtonClicked(absolutePath)
+                        }
                         
                         Repeater {
                             id: moreActionsButtonContextMenuRepeater
@@ -230,31 +257,6 @@ MouseArea {
                     }
                 }
             }
-            
-            Button {
-                id: shareButton
-                
-                Layout.preferredWidth: (path === "") ? 0 : parent.height
-                Layout.preferredHeight: parent.height
-                Layout.alignment: Qt.AlignRight
-                flat: true
-                hoverEnabled: true
-                visible: (path === "") ? false : true
-                display: AbstractButton.IconOnly
-                icon.source: "qrc:///client/theme/share.svg"
-                icon.color: "transparent"
-                background: Rectangle {
-                    color: parent.hovered ? Style.lightHover : "transparent"
-                }
-                ToolTip.visible: hovered
-                ToolTip.delay: 1000
-                ToolTip.text: qsTr("Open share dialog")
-                onClicked: Systray.openShareDialog(displayPath,absolutePath)
-                
-                Accessible.role: Accessible.Button
-                Accessible.name: qsTr("Share %1").arg(displayPath)
-                Accessible.onPressAction: shareButton.clicked()
-            }
         }
     }
 }

+ 36 - 0
src/gui/tray/ActivityList.qml

@@ -0,0 +1,36 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+import Style 1.0
+
+import com.nextcloud.desktopclient 1.0 as NC
+
+ScrollView {
+    property alias model: activityList.model
+
+    signal showFileActivity(string displayPath, string absolutePath)
+    signal activityItemClicked(int index)
+
+    contentWidth: availableWidth
+
+    ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+    ListView {
+        id: activityList
+
+        keyNavigationEnabled: true
+
+        Accessible.role: Accessible.List
+        Accessible.name: qsTr("Activity list")
+
+        clip: true
+
+        delegate: ActivityItem {
+            width: activityList.contentWidth
+            height: Style.trayWindowHeaderHeight
+            flickable: activityList
+            onClicked: activityItemClicked(model.index)
+            onFileActivityButtonClicked: showFileActivity(displayPath, absolutePath)
+        }
+    }
+}

+ 50 - 6
src/gui/tray/ActivityListModel.cpp

@@ -18,6 +18,7 @@
 #include <QWidget>
 #include <QJsonObject>
 #include <QJsonDocument>
+#include <qloggingcategory.h>
 
 #include "account.h"
 #include "accountstate.h"
@@ -39,7 +40,13 @@ namespace OCC {
 
 Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
 
-ActivityListModel::ActivityListModel(AccountState *accountState, QObject *parent)
+ActivityListModel::ActivityListModel(QObject *parent)
+    : QAbstractListModel(parent)
+{
+}
+
+ActivityListModel::ActivityListModel(AccountState *accountState,
+    QObject *parent)
     : QAbstractListModel(parent)
     , _accountState(accountState)
 {
@@ -60,9 +67,40 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
     roles[ActionTextColorRole] = "activityTextTitleColor";
     roles[ObjectTypeRole] = "objectType";
     roles[PointInTimeRole] = "dateTime";
+    roles[DisplayActions] = "displayActions";
     return roles;
 }
 
+void ActivityListModel::setAccountState(AccountState *state)
+{
+    _accountState = state;
+}
+
+void ActivityListModel::setCurrentlyFetching(bool value)
+{
+    _currentlyFetching = value;
+}
+
+bool ActivityListModel::currentlyFetching() const
+{
+    return _currentlyFetching;
+}
+
+void ActivityListModel::setDoneFetching(bool value)
+{
+    _doneFetching = value;
+}
+
+void ActivityListModel::setHideOldActivities(bool value)
+{
+    _hideOldActivities = value;
+}
+
+void ActivityListModel::setDisplayActions(bool value)
+{
+    _displayActions = value;
+}
+
 QVariant ActivityListModel::data(const QModelIndex &index, int role) const
 {
     Activity a;
@@ -222,6 +260,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         return a._id == -1 ? "" : Utility::timeAgoInWords(a._dateTime.toLocalTime());
     case AccountConnectedRole:
         return (ast && ast->isConnected());
+    case DisplayActions:
+        return _displayActions;
     default:
         return QVariant();
     }
@@ -253,7 +293,7 @@ void ActivityListModel::startFetchJob()
     }
     auto *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
     QObject::connect(job, &JsonApiJob::jsonReceived,
-        this, &ActivityListModel::slotActivitiesReceived);
+        this, &ActivityListModel::activitiesReceived);
 
     QUrlQuery params;
     params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
@@ -265,7 +305,7 @@ void ActivityListModel::startFetchJob()
     job->start();
 }
 
-void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int statusCode)
+void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
 {
     auto activities = json.object().value("ocs").toObject().value("data").toArray();
 
@@ -304,9 +344,8 @@ void ActivityListModel::slotActivitiesReceived(const QJsonDocument &json, int st
         _currentItem = list.last()._id;
 
         _totalActivitiesFetched++;
-        if(_totalActivitiesFetched == _maxActivities ||
-            a._dateTime < oldestDate) {
-
+        if (_totalActivitiesFetched == _maxActivities
+            || (_hideOldActivities && a._dateTime < oldestDate)) {
             _showMoreActivitiesAvailableEntry = true;
             _doneFetching = true;
             break;
@@ -496,6 +535,11 @@ void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
     emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
 }
 
+AccountState *ActivityListModel::accountState() const
+{
+    return _accountState;
+}
+
 void ActivityListModel::combineActivityLists()
 {
     ActivityList resultList;

+ 45 - 25
src/gui/tray/ActivityListModel.h

@@ -39,26 +39,34 @@ class InvalidFilenameDialog;
 class ActivityListModel : public QAbstractListModel
 {
     Q_OBJECT
+
+    Q_PROPERTY(AccountState *accountState READ accountState CONSTANT)
 public:
     enum DataRole {
-    ActionIconRole = Qt::UserRole + 1,
-    UserIconRole,
-    AccountRole,
-    ObjectTypeRole,
-    ActionsLinksRole,
-    ActionTextRole,
-    ActionTextColorRole,
-    ActionRole,
-    MessageRole,
-    DisplayPathRole,
-    PathRole,
-    AbsolutePathRole,
-    LinkRole,
-    PointInTimeRole,
-    AccountConnectedRole,
-    SyncFileStatusRole};
-
-    explicit ActivityListModel(AccountState *accountState, QObject* parent = nullptr);
+        ActionIconRole = Qt::UserRole + 1,
+        UserIconRole,
+        AccountRole,
+        ObjectTypeRole,
+        ActionsLinksRole,
+        ActionTextRole,
+        ActionTextColorRole,
+        ActionRole,
+        MessageRole,
+        DisplayPathRole,
+        PathRole,
+        AbsolutePathRole,
+        LinkRole,
+        PointInTimeRole,
+        AccountConnectedRole,
+        SyncFileStatusRole,
+        DisplayActions,
+    };
+    Q_ENUM(DataRole)
+
+    explicit ActivityListModel(QObject *parent = nullptr);
+
+    explicit ActivityListModel(AccountState *accountState,
+        QObject *parent = nullptr);
 
     QVariant data(const QModelIndex &index, int role) const override;
     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
@@ -79,22 +87,30 @@ public:
     Q_INVOKABLE void triggerDefaultAction(int activityIndex);
     Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
 
+    AccountState *accountState() const;
+
 public slots:
     void slotRefreshActivity();
     void slotRemoveAccount();
 
-private slots:
-    void slotActivitiesReceived(const QJsonDocument &json, int statusCode);
-
 signals:
     void activityJobStatusCode(int statusCode);
     void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
 
 protected:
+    void activitiesReceived(const QJsonDocument &json, int statusCode);
     QHash<int, QByteArray> roleNames() const override;
 
+    void setAccountState(AccountState *state);
+    void setCurrentlyFetching(bool value);
+    bool currentlyFetching() const;
+    void setDoneFetching(bool value);
+    void setHideOldActivities(bool value);
+    void setDisplayActions(bool value);
+
+    virtual void startFetchJob();
+
 private:
-    void startFetchJob();
     void combineActivityLists();
     bool canFetchActivities() const;
 
@@ -105,11 +121,10 @@ private:
     Activity _notificationIgnoredFiles;
     ActivityList _notificationErrorsLists;
     ActivityList _finalList;
-    AccountState *_accountState;
-    bool _currentlyFetching = false;
-    bool _doneFetching = false;
     int _currentItem = 0;
 
+    bool _displayActions = true;
+
     int _totalActivitiesFetched = 0;
     int _maxActivities = 100;
     int _maxActivitiesDays = 30;
@@ -117,6 +132,11 @@ private:
 
     QPointer<ConflictDialog> _currentConflictDialog;
     QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
+
+    AccountState *_accountState = nullptr;
+    bool _currentlyFetching = false;
+    bool _doneFetching = false;
+    bool _hideOldActivities = true;
 };
 }
 

+ 21 - 0
src/gui/tray/FileActivityDialog.qml

@@ -0,0 +1,21 @@
+import QtQuick.Window 2.15
+
+import com.nextcloud.desktopclient 1.0 as NC
+
+Window {
+    id: dialog
+
+    property alias model: activityModel
+
+    NC.FileActivityListModel {
+        id: activityModel
+    }   
+
+    width: 500
+    height: 500
+
+    ActivityList {
+        anchors.fill: parent
+        model: dialog.model
+    }
+}

+ 43 - 23
src/gui/tray/Window.qml

@@ -22,8 +22,15 @@ Window {
     flags:      Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint
 
 
+    property var fileActivityDialogAbsolutePath: ""
     readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth
 
+    function openFileActivityDialog(displayPath, absolutePath) {
+        fileActivityDialogLoader.displayPath = displayPath
+        fileActivityDialogLoader.absolutePath = absolutePath
+        fileActivityDialogLoader.refresh()
+    }
+
     Component.onCompleted: Systray.forceWindowInit(trayWindow)
 
     // Close tray window when focus is lost (e.g. click somewhere else on the screen)
@@ -70,6 +77,10 @@ Window {
             trayWindow.hide();
             Systray.setClosed();
         }
+
+        function onShowFileActivityDialog(displayPath, absolutePath) {
+            openFileActivityDialog(displayPath, absolutePath)
+        }
     }
 
     OpacityMask {
@@ -553,31 +564,40 @@ Window {
             }
         }   // Rectangle trayWindowHeaderBackground
 
-        ListView {
-            id: activityListView
-            anchors.top: trayWindowHeaderBackground.bottom
-            anchors.left: trayWindowBackground.left
-            anchors.right: trayWindowBackground.right
-            anchors.bottom: trayWindowBackground.bottom
-            clip: true
-            ScrollBar.vertical: ScrollBar {
-                id: listViewScrollbar
+       ActivityList {
+           anchors.top: trayWindowHeaderBackground.bottom
+           anchors.left: trayWindowBackground.left
+           anchors.right: trayWindowBackground.right
+           anchors.bottom: trayWindowBackground.bottom
+           
+           model: activityModel
+           onShowFileActivity: {
+               openFileActivityDialog(displayPath, absolutePath)
+           }
+           onActivityItemClicked: {
+               model.triggerDefaultAction(index)
+           }
+       }
+
+        Loader {
+            id: fileActivityDialogLoader
+
+            property string displayPath: ""
+            property string absolutePath: ""
+
+            function refresh() {
+                active = true
+                item.model.load(activityModel.accountState, absolutePath)
+                item.show()            
             }
 
-            readonly property int maxActionButtons: 2
-
-            keyNavigationEnabled: true
-
-            Accessible.role: Accessible.List
-            Accessible.name: qsTr("Activity list")
-
-            model: activityModel
-
-            delegate: ActivityItem {  
-                width: activityListView.width
-                height: Style.trayWindowHeaderHeight
-                onClicked: activityModel.triggerDefaultAction(model.index)
+            active: false
+            sourceComponent: FileActivityDialog {
+                title: qsTr("%1 - File activity").arg(fileActivityDialogLoader.displayPath)
+                onClosing: fileActivityDialogLoader.active = false
             }
+
+            onLoaded: refresh()
         }
-    }       // Rectangle trayWindowBackground
+    } // Rectangle trayWindowBackground
 }