Просмотр исходного кода

Merge pull request #5377 from nextcloud/feature/optimalLayoutForNotificationsAndActionButtons

change size of dismiss button for items in activity list
Matthieu Gallien 2 лет назад
Родитель
Сommit
b98887b5fb
46 измененных файлов с 546 добавлено и 420 удалено
  1. 5 14
      src/common/utility.cpp
  2. 2 2
      src/gui/BasicComboBox.qml
  3. 14 0
      src/gui/ErrorBox.qml
  4. 1 1
      src/gui/filedetails/FileDetailsPage.qml
  5. 3 3
      src/gui/filedetails/ShareDelegate.qml
  6. 4 4
      src/gui/filedetails/ShareDetailsPage.qml
  7. 1 1
      src/gui/filedetails/ShareView.qml
  8. 0 1
      src/gui/filedetails/sharemodel.cpp
  9. 1 34
      src/gui/filedetails/sortedsharemodel.cpp
  10. 0 11
      src/gui/filedetails/sortedsharemodel.h
  11. 7 4
      src/gui/iconutils.cpp
  12. 3 16
      src/gui/main.cpp
  13. 4 2
      src/gui/owncloudgui.cpp
  14. 1 2
      src/gui/tray/ActivityActionButton.qml
  15. 2 31
      src/gui/tray/ActivityItem.qml
  16. 19 63
      src/gui/tray/ActivityItemActions.qml
  17. 176 90
      src/gui/tray/ActivityItemContent.qml
  18. 1 1
      src/gui/tray/ActivityItemContextMenu.qml
  19. 4 8
      src/gui/tray/ActivityList.qml
  20. 2 2
      src/gui/tray/CallNotificationDialog.qml
  21. 19 5
      src/gui/tray/CustomButton.qml
  22. 21 7
      src/gui/tray/HeaderButton.qml
  23. 2 1
      src/gui/tray/NCButtonContents.qml
  24. 2 4
      src/gui/tray/SyncStatus.qml
  25. 16 2
      src/gui/tray/UnifiedSearchInputContainer.qml
  26. 16 2
      src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml
  27. 18 4
      src/gui/tray/UnifiedSearchResultItem.qml
  28. 15 1
      src/gui/tray/UnifiedSearchResultItemSkeleton.qml
  29. 14 0
      src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml
  30. 15 1
      src/gui/tray/UnifiedSearchResultListItem.qml
  31. 16 2
      src/gui/tray/UnifiedSearchResultNothingFound.qml
  32. 15 1
      src/gui/tray/UnifiedSearchResultSectionItem.qml
  33. 18 4
      src/gui/tray/UserLine.qml
  34. 18 4
      src/gui/tray/Window.qml
  35. 3 3
      src/gui/tray/activitydata.cpp
  36. 62 20
      src/gui/tray/activitydata.h
  37. 7 10
      src/gui/tray/activitylistmodel.cpp
  38. 2 2
      src/gui/tray/activitylistmodel.h
  39. 3 3
      src/gui/tray/notificationhandler.cpp
  40. 1 31
      src/gui/tray/sortedactivitylistmodel.cpp
  41. 0 12
      src/gui/tray/sortedactivitylistmodel.h
  42. 1 1
      test/testactivitydata.cpp
  43. 3 3
      test/testactivitylistmodel.cpp
  44. 4 4
      test/testsortedsharemodel.cpp
  45. 3 3
      test/testutility.cpp
  46. 2 0
      theme/Style/Style.qml

+ 5 - 14
src/common/utility.cpp

@@ -478,10 +478,8 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
         now = from;
     }
 
-    if (dt.daysTo(now) == 1) {
-        return QObject::tr("%n day ago", "", dt.daysTo(now));
-    } else if (dt.daysTo(now) > 1) {
-        return QObject::tr("%n days ago", "", dt.daysTo(now));
+    if (dt.daysTo(now) >= 1) {
+        return QObject::tr("%nd", "delay in days after an activity", dt.daysTo(now));
     } else {
         qint64 secs = dt.secsTo(now);
         if (secs < 0) {
@@ -490,11 +488,7 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
 
         if (floor(secs / 3600.0) > 0) {
             int hours = floor(secs / 3600.0);
-            if (hours == 1) {
-                return (QObject::tr("%n hour ago", "", hours));
-            } else {
-                return (QObject::tr("%n hours ago", "", hours));
-            }
+            return (QObject::tr("%nh", "delay in hours after an activity", hours));
         } else {
             int minutes = qRound(secs / 60.0);
 
@@ -502,13 +496,10 @@ QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
                 if (secs < 5) {
                     return QObject::tr("now");
                 } else {
-                    return QObject::tr("Less than a minute ago");
+                    return QObject::tr("1m", "one minute after activity date and time");
                 }
-
-            } else if (minutes == 1) {
-                return (QObject::tr("%n minute ago", "", minutes));
             } else {
-                return (QObject::tr("%n minutes ago", "", minutes));
+                return (QObject::tr("%nm", "delay in minutes after an activity", minutes));
             }
         }
     }

+ 2 - 2
src/gui/BasicComboBox.qml

@@ -1,5 +1,5 @@
 /*
- * Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
+ * Copyright (C) 2022 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
@@ -15,7 +15,7 @@
 import QtQuick 2.15
 import QtQuick.Layouts 1.15
 import QtQuick.Controls 2.15
-import QtGraphicalEffects 1.0
+import QtGraphicalEffects 1.15
 
 import Style 1.0
 import "./tray"

+ 14 - 0
src/gui/ErrorBox.qml

@@ -1,3 +1,17 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQuick 2.15
 import QtQuick.Layouts 1.15
 import QtQuick.Controls 2.15

+ 1 - 1
src/gui/filedetails/FileDetailsPage.qml

@@ -134,7 +134,7 @@ Page {
                 Layout.preferredHeight: width
                 Layout.rightMargin: headerGridLayout.textRightMargin
 
-                imageSource: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
+                icon.source: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
                 bgColor: Style.lightHover
                 bgNormalOpacity: 0
                 toolTipText: qsTr("Dismiss")

+ 3 - 3
src/gui/filedetails/ShareDelegate.qml

@@ -164,7 +164,7 @@ GridLayout {
             bgColor: Style.lightHover
             bgNormalOpacity: 0
 
-            imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncTextColor
+            icon.source: "image://svgimage-custom-color/add.svg/" + Style.ncTextColor
 
             visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares
             enabled: visible
@@ -199,7 +199,7 @@ GridLayout {
             bgColor: shareLinkCopied ? Style.positiveColor : Style.lightHover
             bgNormalOpacity: shareLinkCopied ? 1 : 0
 
-            imageSource: shareLinkCopied ? "image://svgimage-custom-color/copy.svg/" + Style.ncHeaderTextColor :
+            icon.source: shareLinkCopied ? "image://svgimage-custom-color/copy.svg/" + Style.ncHeaderTextColor :
                                            "image://svgimage-custom-color/copy.svg/" + Style.ncTextColor
             icon.width: 16
             icon.height: 16
@@ -245,7 +245,7 @@ GridLayout {
             bgColor: Style.lightHover
             bgNormalOpacity: 0
 
-            imageSource: "image://svgimage-custom-color/more.svg/" + Style.ncTextColor
+            icon.source: "image://svgimage-custom-color/more.svg/" + Style.ncTextColor
 
             visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare
             enabled: visible

+ 4 - 4
src/gui/filedetails/ShareDetailsPage.qml

@@ -230,7 +230,7 @@ Page {
                 Layout.preferredHeight: width
                 Layout.rightMargin: root.padding
 
-                imageSource: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
+                icon.source: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
                 bgColor: Style.lightHover
                 bgNormalOpacity: 0
                 toolTipText: qsTr("Dismiss")
@@ -806,7 +806,7 @@ Page {
             CustomButton {
                 height: Style.standardPrimaryButtonHeight
 
-                imageSource: "image://svgimage-custom-color/close.svg/" + Style.errorBoxBackgroundColor
+                icon.source: "image://svgimage-custom-color/close.svg/" + Style.errorBoxBackgroundColor
                 imageSourceHover: "image://svgimage-custom-color/close.svg/" + Style.ncHeaderTextColor
                 text: qsTr("Unshare")
                 textColor: Style.errorBoxBackgroundColor
@@ -823,7 +823,7 @@ Page {
             CustomButton {
                 height: Style.standardPrimaryButtonHeight
 
-                imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncBlue
+                icon.source: "image://svgimage-custom-color/add.svg/" + Style.ncBlue
                 imageSourceHover: "image://svgimage-custom-color/add.svg/" + Style.ncHeaderTextColor
                 text: qsTr("Add another link")
                 textColor: Style.ncBlue
@@ -867,7 +867,7 @@ Page {
 
             height: Style.standardPrimaryButtonHeight
 
-            imageSource: "image://svgimage-custom-color/copy.svg/" + Style.ncHeaderTextColor
+            icon.source: "image://svgimage-custom-color/copy.svg/" + Style.ncHeaderTextColor
             text: shareLinkCopied ? qsTr("Share link copied!") : qsTr("Copy share link")
             textColor: Style.ncHeaderTextColor
             contentsFont.bold: true

+ 1 - 1
src/gui/filedetails/ShareView.qml

@@ -195,7 +195,7 @@ ColumnLayout {
 
                 enabled: !root.loading
                 model: SortedShareModel {
-                    shareModel: root.shareModel
+                    sourceModel: root.shareModel
                 }
 
                 delegate: ShareDelegate {

+ 0 - 1
src/gui/filedetails/sharemodel.cpp

@@ -199,7 +199,6 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
         return {};
     }
 
-    qCWarning(lcShareModel) << "Got unknown role" << role << "for share of type" << share->getShareType() << "so returning null value.";
     return {};
 }
 

+ 1 - 34
src/gui/filedetails/sortedsharemodel.cpp

@@ -21,40 +21,7 @@ Q_LOGGING_CATEGORY(lcSortedShareModel, "com.nextcloud.sortedsharemodel")
 SortedShareModel::SortedShareModel(QObject *parent)
     : QSortFilterProxyModel(parent)
 {
-}
-
-void SortedShareModel::sortModel()
-{
-    sort(0);
-}
-
-ShareModel *SortedShareModel::shareModel() const
-{
-    return qobject_cast<ShareModel*>(sourceModel());
-}
-
-void SortedShareModel::setShareModel(ShareModel *shareModel)
-{
-    const auto currentSetModel = sourceModel();
-
-    if(currentSetModel) {
-        disconnect(currentSetModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel);
-        disconnect(currentSetModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel);
-        disconnect(currentSetModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel);
-        disconnect(currentSetModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel);
-        disconnect(currentSetModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel);
-    }
-
-    // Re-sort model when any changes take place
-    connect(shareModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel);
-    connect(shareModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel);
-    connect(shareModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel);
-    connect(shareModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel);
-    connect(shareModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel);
-
-    setSourceModel(shareModel);
-    sortModel();
-    Q_EMIT shareModelChanged();
+    sort(0, Qt::AscendingOrder);
 }
 
 bool SortedShareModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const

+ 0 - 11
src/gui/filedetails/sortedsharemodel.h

@@ -22,24 +22,13 @@ namespace OCC {
 class SortedShareModel : public QSortFilterProxyModel
 {
     Q_OBJECT
-    Q_PROPERTY(ShareModel* shareModel READ shareModel WRITE setShareModel NOTIFY shareModelChanged)
 
 public:
     explicit SortedShareModel(QObject *parent = nullptr);
 
-    [[nodiscard]] ShareModel *shareModel() const;
-
-signals:
-    void shareModelChanged();
-
-public slots:
-    void setShareModel(OCC::ShareModel *shareModel);
-
 protected:
     [[nodiscard]] bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
 
-private slots:
-    void sortModel();
 };
 
 } // namespace OCC

+ 7 - 4
src/gui/iconutils.cpp

@@ -67,7 +67,7 @@ QImage createSvgImageWithCustomColor(const QString &fileName, const QColor &cust
     QImage result{};
 
     if (fileName.isEmpty() || !customColor.isValid()) {
-        qWarning(lcIconUtils) << "invalid fileName or customColor";
+        qCWarning(lcIconUtils) << "invalid fileName or customColor";
         return result;
     }
 
@@ -106,7 +106,7 @@ QImage createSvgImageWithCustomColor(const QString &fileName, const QColor &cust
 
     Q_ASSERT(!sourceSvg.isEmpty());
     if (sourceSvg.isEmpty()) {
-        qWarning(lcIconUtils) << "Failed to find base SVG file for" << fileName;
+        qCWarning(lcIconUtils) << "Failed to find base SVG file for" << fileName;
         return result;
     }
 
@@ -114,7 +114,7 @@ QImage createSvgImageWithCustomColor(const QString &fileName, const QColor &cust
 
     Q_ASSERT(!result.isNull());
     if (result.isNull()) {
-        qWarning(lcIconUtils) << "Failed to load pixmap for" << fileName;
+        qCWarning(lcIconUtils) << "Failed to load pixmap for" << fileName;
     }
 
     return result;
@@ -155,7 +155,10 @@ QImage drawSvgWithCustomFillColor(
         return {};
     }
 
-    const auto reqSize = requestedSize.isValid() ? requestedSize : svgRenderer.defaultSize();
+    const auto reqSize = (requestedSize.isValid() && requestedSize.height() && requestedSize.height()) ? requestedSize : svgRenderer.defaultSize();
+    if (!reqSize.isValid() || !reqSize.height() || !reqSize.height()) {
+        return {};
+    }
 
     if (originalSize) {
         *originalSize = svgRenderer.defaultSize();

+ 3 - 16
src/gui/main.cpp

@@ -61,13 +61,6 @@ int main(int argc, char **argv)
     Q_INIT_RESOURCE(resources);
     Q_INIT_RESOURCE(theme);
 
-    // Work around a bug in KDE's qqc2-desktop-style which breaks
-    // buttons with icons not based on a name, by forcing a style name
-    // the platformtheme plugin won't try to force qqc2-desktops-style
-    // anymore.
-    // Can be removed once the bug in qqc2-desktop-style is gone.
-    QQuickStyle::setStyle("Fusion");
-
     // OpenSSL 1.1.0: No explicit initialisation or de-initialisation is necessary.
 
     QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true);
@@ -80,6 +73,9 @@ int main(int argc, char **argv)
     surfaceFormat.setOption(QSurfaceFormat::ResetNotification);
     QSurfaceFormat::setDefaultFormat(surfaceFormat);
 
+    QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
+    QQuickStyle::setStyle(QStringLiteral("Fusion"));
+
     OCC::Application app(argc, argv);
 
 #ifdef Q_OS_WIN
@@ -104,15 +100,6 @@ int main(int argc, char **argv)
         return 0;
     }
 
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
-    QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
-#else
-    // See https://bugreports.qt.io/browse/QTBUG-70481
-    if (std::fmod(app.devicePixelRatio(), 1) == 0) {
-        QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
-    }
-#endif
-
 // check a environment variable for core dumps
 #ifdef Q_OS_UNIX
     if (!qEnvironmentVariableIsEmpty("OWNCLOUD_CORE_DUMP")) {

+ 4 - 2
src/gui/owncloudgui.cpp

@@ -54,7 +54,7 @@
 #include <QtDBus/QDBusInterface>
 #endif
 
-
+#include <QAbstractItemModel>
 #include <QQmlEngine>
 #include <QQmlComponent>
 #include <QQmlApplicationEngine>
@@ -128,13 +128,15 @@ ownCloudGui::ownCloudGui(Application *parent)
     qmlRegisterType<SortedShareModel>("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
     qmlRegisterType<SyncConflictsModel>("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel");
 
+    qmlRegisterUncreatableType<QAbstractItemModel>("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel");
+    qmlRegisterUncreatableType<Activity>("com.nextcloud.desktopclient", 1, 0, "Activity", "Activity");
+    qmlRegisterUncreatableType<TalkNotificationData>("com.nextcloud.desktopclient", 1, 0, "TalkNotificationData", "TalkNotificationData");
     qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
     qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
     qmlRegisterUncreatableType<Sharee>("com.nextcloud.desktopclient", 1, 0, "Sharee", "Access to Type enum");
 
     qRegisterMetaTypeStreamOperators<Emoji>();
 
-    qRegisterMetaType<ActivityListModel *>("ActivityListModel*");
     qRegisterMetaType<UnifiedSearchResultsListModel *>("UnifiedSearchResultsListModel*");
     qRegisterMetaType<UserStatus>("UserStatus");
     qRegisterMetaType<SharePtr>("SharePtr");

+ 1 - 2
src/gui/tray/ActivityActionButton.qml

@@ -10,7 +10,6 @@ AbstractButton {
 
     property bool primaryButton: false
 
-    property string imageSource: ""
     property string imageSourceHover: ""
 
     property color adjustedHeaderColor: Style.adjustedCurrentUserHeaderColor
@@ -69,7 +68,7 @@ AbstractButton {
             anchors.fill: parent
             hovered: root.hovered
             imageSourceHover: root.imageSourceHover
-            imageSource: root.imageSource
+            imageSource: root.icon.source
             text: root.text
             textColor: root.textColor
             textColorHovered: root.textColorHovered

+ 2 - 31
src/gui/tray/ActivityItem.qml

@@ -18,7 +18,6 @@ ItemDelegate {
     readonly property bool isTalkReplyPossible: model.conversationToken !== ""
     property bool isTalkReplyOptionVisible: model.messageSent !== ""
 
-    enabled: (model.path !== "" || model.link !== "" || model.links.length > 0 ||  model.isCurrentUserFileActivity === true)
     padding: Style.standardSpacing
 
     Accessible.role: Accessible.ListItem
@@ -31,12 +30,6 @@ ItemDelegate {
     }
 
     contentItem: ColumnLayout {
-        id: contentLayout
-        anchors.left: root.left
-        anchors.right: root.right
-        anchors.rightMargin: Style.standardSpacing
-        anchors.leftMargin: Style.standardSpacing
-
         spacing: Style.activityContentSpace
 
         ActivityItemContent {
@@ -50,6 +43,7 @@ ItemDelegate {
             iconSize: root.iconSize
 
             activityData: model
+            activity: model.activity
 
             onDismissButtonClicked: activityModel.slotTriggerDismiss(model.activityIndex)
         }
@@ -61,7 +55,7 @@ ItemDelegate {
 
             Layout.preferredWidth: Style.talkReplyTextFieldPreferredWidth
             Layout.preferredHeight: Style.talkReplyTextFieldPreferredHeight
-            Layout.leftMargin: Style.trayListItemIconSize + activityContent.spacing
+            Layout.leftMargin: Style.trayListItemIconSize + Style.trayHorizontalMargin
 
             sourceComponent: TalkReplyTextField {
                 onSendReply: {
@@ -70,28 +64,5 @@ ItemDelegate {
                 }
             }
         }
-
-        ActivityItemActions {
-            id: activityActions
-
-            visible: !root.isFileActivityList && model.linksForActionButtons.length > 1 && !isTalkReplyOptionVisible
-
-            Layout.fillWidth: true
-            Layout.leftMargin: Style.trayListItemIconSize + activityContent.spacing
-            Layout.preferredHeight: Style.standardPrimaryButtonHeight
-
-            displayActions: model.displayActions
-            objectType: model.objectType
-            linksForActionButtons: model.linksForActionButtons
-            linksContextMenu: model.linksContextMenu
-
-            maxActionButtons: activityModel.maxActionButtons
-
-            flickable: root.flickable
-
-            onTriggerAction: activityModel.slotTriggerAction(model.activityIndex, actionIndex)
-
-            onShowReplyField: root.isTalkReplyOptionVisible = true
-        }
     }
 }

+ 19 - 63
src/gui/tray/ActivityItemActions.qml

@@ -1,15 +1,13 @@
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import Style 1.0
 import com.nextcloud.desktopclient 1.0
 
-RowLayout {
+Repeater {
     id: root
 
-    spacing: 20
-
     property string objectType: ""
     property variant linksForActionButtons: []
     property variant linksContextMenu: []
@@ -24,71 +22,29 @@ RowLayout {
     signal triggerAction(int actionIndex)
     signal showReplyField()
 
-    Repeater {
-        id: actionsRepeater
-        // a max of maxActionButtons will get dispayed as separate buttons
-        model: root.linksForActionButtons
-
-        ActivityActionButton {
-            id: activityActionButton
-
-            Layout.minimumWidth: primaryButton ? Style.activityItemActionPrimaryButtonMinWidth : Style.activityItemActionSecondaryButtonMinWidth
-            Layout.preferredHeight: parent.height
-            Layout.preferredWidth: primaryButton ? -1 : parent.height
-
-            verb: model.modelData.verb
-            primaryButton: (model.index === 0 && verb !== "DELETE") || model.modelData.primary
-            isTalkReplyButton: verb === "REPLY"
-
-            text: model.modelData.label
-
-            adjustedHeaderColor: Style.adjustedCurrentUserHeaderColor
-
-            imageSource: model.modelData.imageSource ? model.modelData.imageSource + Style.adjustedCurrentUserHeaderColor : ""
-            imageSourceHover: model.modelData.imageSourceHovered ? model.modelData.imageSourceHovered + Style.currentUserHeaderTextColor : ""
-
-            onClicked: isTalkReplyButton ? root.showReplyField() : root.triggerAction(model.index)
-        }
-    }
-
-    Loader {
-        // actions that do not fit maxActionButtons limit, must be put into a context menu
-        id: moreActionsButtonContainer
-
-        Layout.preferredWidth: parent.height
-        Layout.topMargin: Style.roundedButtonBackgroundVerticalMargins
-        Layout.bottomMargin: Style.roundedButtonBackgroundVerticalMargins
-        Layout.fillHeight: true
-
-        active: root.displayActions && (root.linksContextMenu.length > 0)
+    model: root.linksForActionButtons
 
-        sourceComponent: Button {
-            id: moreActionsButton
+    CustomButton {
+        id: activityActionButton
 
-            icon.source: "qrc:///client/theme/more.svg"
-            icon.color: Style.ncTextColor
+        property string verb: model.modelData.verb
+        property bool isTalkReplyButton: verb === "REPLY"
 
-            background: Rectangle {
-                color: parent.hovered ? Style.lightHover : root.moreActionsButtonColor
-                radius: width / 2
-            }
+        Layout.alignment: Qt.AlignTop | Qt.AlignRight
 
-            NCToolTip {
-                visible: parent.hovered
-                text: qsTr("Show more actions")
-            }
+        hoverEnabled: true
+        padding: Style.smallSpacing
+        display: Button.TextOnly
 
-            Accessible.name: qsTr("Show more actions")
+        text: model.modelData.label
 
-            onClicked:  moreActionsButtonContextMenu.popup(moreActionsButton.x, moreActionsButton.y);
+        icon.source: model.modelData.imageSource ? model.modelData.imageSource + Style.adjustedCurrentUserHeaderColor : ""
 
-            Connections {
-                target: root.flickable
+        onClicked: isTalkReplyButton ? root.showReplyField() : root.triggerAction(model.index)
 
-                function onMovementStarted() {
-                    moreActionsButtonContextMenu.close();
-                }
-            }
-        }
+        textColor: Style.adjustedCurrentUserHeaderColor
+        textColorHovered: Style.currentUserHeaderTextColor
+        contentsFont.bold: true
+        bgColor: Style.currentUserHeaderColor
     }
 }

+ 176 - 90
src/gui/tray/ActivityItemContent.qml

@@ -1,16 +1,18 @@
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
-import Style 1.0
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import QtGraphicalEffects 1.15
+import Style 1.0
 import com.nextcloud.desktopclient 1.0
 
-RowLayout {
+Item {
     id: root
 
     property variant activityData: {{}}
 
+    property variant activity: {{}}
+
     property color activityTextTitleColor: Style.ncTextColor
 
     property bool showDismissButton: false
@@ -21,17 +23,20 @@ RowLayout {
 
     signal dismissButtonClicked()
 
-    spacing: Style.trayHorizontalMargin
-
     Item {
         id: thumbnailItem
-        Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
-        Layout.preferredWidth: root.iconSize
-        Layout.preferredHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? root.iconSize * 0.9 : root.iconSize
+
         readonly property int imageWidth: width * (1 - Style.thumbnailImageSizeReduction)
         readonly property int imageHeight: height * (1 - Style.thumbnailImageSizeReduction)
         readonly property int thumbnailRadius: model.thumbnail && model.thumbnail.isUserAvatar ? width / 2 : 3
 
+        anchors.left: parent.left
+        anchors.top: parent.top
+        anchors.bottom: parent.bottom
+
+        implicitHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? root.iconSize * 0.9 : root.iconSize
+        implicitWidth: root.iconSize
+
         Loader {
             id: thumbnailImageLoader
             anchors.fill: parent
@@ -110,102 +115,183 @@ RowLayout {
         }
     }
 
-    Column {
-        id: activityTextColumn
-
-        Layout.topMargin: Style.activityContentSpace
-        Layout.fillWidth: true
-        Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-
-        spacing: Style.activityContentSpace
-
-        EnforcedPlainTextLabel {
-            id: activityTextTitle
-            text: (root.activityData.type === "Activity" || root.activityData.type === "Notification") ? root.activityData.subject : root.activityData.message
-            height: (text === "") ? 0 : implicitHeight
-            width: parent.width
-            elide: Text.ElideRight
-            wrapMode: Text.Wrap
-            maximumLineCount: 2
-            font.pixelSize: Style.topLinePixelSize
-            color: Style.ncTextColor
-            visible: text !== ""
-        }
+    ColumnLayout {
+        id: activityContentLayout
 
-        EnforcedPlainTextLabel {
-            id: activityTextInfo
-            text: (root.activityData.type === "Sync") ? root.activityData.displayPath
-                                    : (root.activityData.type === "File") ? root.activityData.subject
-                                                        : (root.activityData.type === "Notification") ? root.activityData.message
-                                                                                    : ""
-            height: (text === "") ? 0 : implicitHeight
-            width: parent.width
-            elide: Text.ElideRight
-            wrapMode: Text.Wrap
-            maximumLineCount: 2
-            font.pixelSize: Style.subLinePixelSize
-            color: Style.ncTextColor
-            visible: text !== ""
-        }
+        anchors.left: thumbnailItem.right
+        anchors.right: parent.right
+        anchors.top: parent.top
+        anchors.bottom: parent.bottom
 
-        EnforcedPlainTextLabel {
-            id: activityTextDateTime
-            text: root.activityData.dateTime
-            height: (text === "") ? 0 : implicitHeight
-            width: parent.width
-            elide: Text.ElideRight
-            wrapMode: Text.Wrap
-            maximumLineCount: 2
-            font.pixelSize: Style.subLinePixelSize
-            color: Style.ncSecondaryTextColor
-            visible: text !== ""
-        }
+        spacing: Style.smallSpacing
+
+        RowLayout {
+            Layout.fillWidth: true
+            Layout.maximumWidth: activityContentLayout.width
+
+            spacing: Style.trayHorizontalMargin
+
+            EnforcedPlainTextLabel {
+                id: activityTextTitle
+                text: (root.activityData.type === "Activity" || root.activityData.type === "Notification") ? root.activityData.subject : root.activityData.message
+                height: (text === "") ? 0 : implicitHeight
+
+                Layout.maximumWidth: activityContentLayout.width - Style.trayHorizontalMargin -
+                                     (activityTextDateTime.visible ? activityTextDateTime.width + Style.trayHorizontalMargin : 0) -
+                                     (dismissActionButton.visible ? dismissActionButton.width + Style.trayHorizontalMargin : 0)
+                Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+
+                elide: Text.ElideRight
+                wrapMode: Text.Wrap
+                maximumLineCount: 1
+                font.pixelSize: Style.topLinePixelSize
+                color: Style.ncTextColor
+                visible: text !== ""
+
+                NCToolTip {
+                    text: parent.text
+                    visible: parent.hovered
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+                Layout.leftMargin: -Style.trayHorizontalMargin
+            }
+
+            EnforcedPlainTextLabel {
+                id: activityTextDateTime
+
+                Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+                height: (text === "") ? 0 : implicitHeight
+                width: parent.width
+
+                text: root.activityData.dateTime
+                elide: Text.ElideRight
+                wrapMode: Text.Wrap
+                maximumLineCount: 2
+                font.pixelSize: Style.subLinePixelSize
+                color: Style.ncSecondaryTextColor
+                visible: text !== ""
+            }
 
-        EnforcedPlainTextLabel {
-            id: talkReplyMessageSent
-            text: root.activityData.messageSent
-            height: (text === "") ? 0 : implicitHeight
-            width: parent.width
-            elide: Text.ElideRight
-            wrapMode: Text.Wrap
-            maximumLineCount: 2
-            font.pixelSize: Style.topLinePixelSize
-            color: Style.ncSecondaryTextColor
-            visible: text !== ""
+            RoundButton {
+                id: dismissActionButton
+
+                Layout.preferredWidth: Style.dismissButtonSize
+                Layout.preferredHeight: Style.dismissButtonSize
+                Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+
+                visible: root.showDismissButton && !fileDetailsButton.visible
+
+                icon.source: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
+
+                flat: true
+                display: Button.IconOnly
+                hoverEnabled: true
+                padding: 0
+
+                NCToolTip {
+                    text: qsTr("Dismiss")
+                    visible: parent.hovered
+                }
+
+                onClicked: root.dismissButtonClicked()
+            }
         }
-    }
 
-    CustomButton {
-        id: dismissActionButton
+        RowLayout {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Layout.minimumHeight: Style.minimumActivityItemHeight
+            Layout.maximumWidth: root.width - thumbnailItem.width
+            spacing: Style.trayHorizontalMargin
+
+            EnforcedPlainTextLabel {
+                id: activityTextInfo
+
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+
+                text: (root.activityData.type === "Sync") ? root.activityData.displayPath
+                                                          : (root.activityData.type === "File") ? root.activityData.subject
+                                                                                                : (root.activityData.type === "Notification") ? root.activityData.message
+                                                                                                                                              : ""
+                height: (text === "") ? 0 : implicitHeight
+                elide: Text.ElideRight
+                wrapMode: Text.Wrap
+                maximumLineCount: 2
+                font.pixelSize: Style.subLinePixelSize
+                color: Style.ncTextColor
+                visible: text !== ""
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                id: fileDetailsButton
 
-        Layout.preferredWidth: Style.trayListItemIconSize
-        Layout.preferredHeight: Style.trayListItemIconSize
+                Layout.preferredWidth: Style.headerButtonIconSize
+                Layout.preferredHeight: Style.headerButtonIconSize
+                Layout.alignment: Qt.AlignTop | Qt.AlignRight
 
-        visible: root.showDismissButton && !fileDetailsButton.visible
+                icon.source: "image://svgimage-custom-color/more.svg"
 
-        imageSource: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
-        imageSourceHover: "image://svgimage-custom-color/clear.svg" + "/" + UserModel.currentUser.headerTextColor
+                NCToolTip {
+                    text: qsTr("Open file details")
+                    visible: parent.hovered
+                }
 
-        toolTipText: qsTr("Dismiss")
+                flat: true
+                display: Button.IconOnly
+                hoverEnabled: true
+                padding: 0
 
-        bgColor: Style.menuBorder
+                visible: model.showFileDetails
 
-        onClicked: root.dismissButtonClicked()
-    }
+                onClicked: Systray.presentShareViewInTray(model.openablePath)
+            }
+
+            EnforcedPlainTextLabel {
+                id: talkReplyMessageSent
+
+                height: (text === "") ? 0 : implicitHeight
+                width: parent.width
+                Layout.alignment: Qt.AlignTop | Qt.AlignRight
 
-    CustomButton {
-        id: fileDetailsButton
+                text: root.activityData.messageSent
+                elide: Text.ElideRight
+                wrapMode: Text.Wrap
+                maximumLineCount: 2
+                font.pixelSize: Style.topLinePixelSize
+                color: Style.ncSecondaryTextColor
+                visible: text !== ""
+            }
+
+            ActivityItemActions {
+                id: activityActions
+
+                visible: !isFileActivityList && activityData.linksForActionButtons.length > 0 && !isTalkReplyOptionVisible
 
-        Layout.preferredWidth: Style.trayListItemIconSize
-        Layout.preferredHeight: Style.trayListItemIconSize
+                Layout.fillWidth: true
+                Layout.leftMargin: Style.trayListItemIconSize + Style.trayHorizontalMargin
+                Layout.preferredHeight: Style.standardPrimaryButtonHeight
+                Layout.alignment: Qt.AlignTop | Qt.AlignRight
 
-        imageSource: "image://svgimage-custom-color/more.svg" + "/" + Style.adjustedCurrentUserHeaderColor
-        imageSourceHover: "image://svgimage-custom-color/more.svg" + "/" + Style.currentUserHeaderTextColor
-        toolTipText: qsTr("Open file details")
-        bgColor: Style.currentUserHeaderColor
+                displayActions: activityData.displayActions
+                objectType: activityData.objectType
+                linksForActionButtons: activityData.linksForActionButtons
+                linksContextMenu: activityData.linksContextMenu
 
-        visible: model.showFileDetails
+                maxActionButtons: activityModel.maxActionButtons
 
-        onClicked: Systray.presentShareViewInTray(model.openablePath)
+                onTriggerAction: activityModel.slotTriggerAction(model.activityIndex, actionIndex)
+
+                onShowReplyField: isTalkReplyOptionVisible = true
+            }
+        }
     }
 }

+ 1 - 1
src/gui/tray/ActivityItemContextMenu.qml

@@ -1,6 +1,6 @@
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
+import QtQuick.Controls 2.15
 import Style 1.0
 
 AutoSizingMenu {

+ 4 - 8
src/gui/tray/ActivityList.qml

@@ -3,12 +3,10 @@ import QtQuick.Controls 2.15
 
 import Style 1.0
 import com.nextcloud.desktopclient 1.0 as NC
-import Style 1.0
 
 ScrollView {
     id: controlRoot
-    property alias model: sortedActivityList.activityListModel
-
+    property alias model: sortedActivityList.sourceModel
     property bool isFileActivityList: false
     property int iconSize: Style.trayListItemIconSize
     property int delegateHorizontalPadding: 0
@@ -44,6 +42,8 @@ ScrollView {
 
             color: Style.lightHover
             visible: activityList.activeFocus
+
+            radius: Style.mediumRoundedButtonRadius
         }
         highlightFollowsCurrentItem: true
         highlightMoveDuration: 0
@@ -54,14 +54,10 @@ ScrollView {
 
         model: NC.SortedActivityListModel {
             id: sortedActivityList
-            activityListModel: controlRoot.model
         }
 
         delegate: ActivityItem {
-            anchors.left: if (parent) parent.left
-            anchors.right: if (parent) parent.right
-            anchors.leftMargin: controlRoot.delegateHorizontalPadding
-            anchors.rightMargin: controlRoot.delegateHorizontalPadding
+            width: activityList.contentItem.width
 
             isFileActivityList: controlRoot.isFileActivityList
             iconSize: controlRoot.iconSize

+ 2 - 2
src/gui/tray/CallNotificationDialog.qml

@@ -225,7 +225,7 @@ Window {
 
                         textColor: Style.ncHeaderTextColor
 
-                        imageSource: root.talkIcon + Style.ncHeaderTextColor
+                        icon.source: root.talkIcon + Style.ncHeaderTextColor
                         imageSourceHover: root.talkIcon + Style.ncHeaderTextColor
 
                         Layout.fillWidth: true
@@ -252,7 +252,7 @@ Window {
 
                     textColor: Style.ncHeaderTextColor
 
-                    imageSource: root.deleteIcon + "white"
+                    icon.source: root.deleteIcon + "white"
                     imageSourceHover: root.deleteIcon + "white"
 
                     Layout.fillWidth: true

+ 19 - 5
src/gui/tray/CustomButton.qml

@@ -1,13 +1,26 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import Style 1.0
 
 Button {
     id: root
 
-    property string imageSource: ""
-    property string imageSourceHover: imageSource
+    property string imageSourceHover: root.icon.source
     property var iconItem: icon
 
     property string toolTipText: ""
@@ -39,9 +52,10 @@ Button {
 
     contentItem: NCButtonContents {
         id: contents
+        display: root.display
         hovered: root.hovered
         imageSourceHover: root.imageSourceHover
-        imageSource: root.imageSource
+        imageSource: root.icon.source
         text: root.text
         textColor: root.textColor
         textColorHovered: root.textColorHovered

+ 21 - 7
src/gui/tray/HeaderButton.qml

@@ -1,10 +1,24 @@
-import QtQml 2.1
-import QtQml.Models 2.1
-import QtQuick 2.9
-import QtQuick.Window 2.3
-import QtQuick.Controls 2.2
-import QtQuick.Layouts 1.2
-import QtGraphicalEffects 1.0
+/*
+ * Copyright (C) 2020 by Nicolas Fella <nicolas.fella@gmx.de>
+ *
+ * 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.
+ */
+
+import QtQml 2.15
+import QtQml.Models 2.15
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.15
 
 // Custom qml modules are in /theme (and included by resources.qrc)
 import Style 1.0

+ 2 - 1
src/gui/tray/NCButtonContents.qml

@@ -25,6 +25,7 @@ RowLayout {
     property string imageSourceHover: ""
     property string imageSource: ""
     property string text: ""
+    property var display
 
     property color textColor: Style.ncTextColor
     property color textColorHovered: textColor
@@ -39,7 +40,7 @@ RowLayout {
         fillMode: Image.PreserveAspectFit
         horizontalAlignment: Image.AlignHCenter
         verticalAlignment: Image.AlignVCenter
-        visible: root.hovered ? root.imageSourceHover !== "" : root.imageSource !== ""
+        visible: root.display === Button.TextOnly ? false : root.hovered ? root.imageSourceHover !== "" : root.imageSource !== ""
     }
 
     EnforcedPlainTextLabel {

+ 2 - 4
src/gui/tray/SyncStatus.qml

@@ -109,13 +109,11 @@ RowLayout {
             font: syncNowButton.contentsFont
         }
 
-        Layout.preferredWidth: syncNowFm.boundingRect(text).width +
-                               leftPadding +
-                               rightPadding +
-                               Style.standardSpacing * 2
         Layout.rightMargin: Style.trayHorizontalMargin
 
         text: qsTr("Sync now")
+
+        padding: Style.smallSpacing
         textColor: Style.adjustedCurrentUserHeaderColor
         textColorHovered: Style.currentUserHeaderTextColor
         contentsFont.bold: true

+ 16 - 2
src/gui/tray/UnifiedSearchInputContainer.qml

@@ -1,7 +1,21 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtGraphicalEffects 1.0
+import QtQuick.Controls 2.15
+import QtGraphicalEffects 1.15
 import Style 1.0
 
 import com.nextcloud.desktopclient 1.0

+ 16 - 2
src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml

@@ -1,7 +1,21 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import Style 1.0
 
 ColumnLayout {

+ 18 - 4
src/gui/tray/UnifiedSearchResultItem.qml

@@ -1,8 +1,22 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
-import QtQuick 2.9
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
-import QtGraphicalEffects 1.0
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.15
 
 import Style 1.0
 

+ 15 - 1
src/gui/tray/UnifiedSearchResultItemSkeleton.qml

@@ -1,6 +1,20 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Layouts 1.2
+import QtQuick.Layouts 1.15
 import QtGraphicalEffects 1.15
 import Style 1.0
 

+ 14 - 0
src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml

@@ -1,3 +1,17 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
 import QtQuick.Layouts 1.15

+ 15 - 1
src/gui/tray/UnifiedSearchResultListItem.qml

@@ -1,6 +1,20 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
+import QtQuick.Controls 2.15
 import Style 1.0
 import com.nextcloud.desktopclient 1.0
 

+ 16 - 2
src/gui/tray/UnifiedSearchResultNothingFound.qml

@@ -1,7 +1,21 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import Style 1.0
 
 ColumnLayout {

+ 15 - 1
src/gui/tray/UnifiedSearchResultSectionItem.qml

@@ -1,7 +1,21 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
 import QtQml 2.15
 import QtQuick 2.15
 import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.2
+import QtQuick.Layouts 1.15
 import Style 1.0
 import com.nextcloud.desktopclient 1.0
 

+ 18 - 4
src/gui/tray/UserLine.qml

@@ -1,7 +1,21 @@
-import QtQuick 2.9
-import QtQuick.Window 2.3
-import QtQuick.Controls 2.2
-import QtQuick.Layouts 1.2
+/*
+ * Copyright (C) 2019 by Dominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.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.
+ */
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 
 // Custom qml modules are in /theme (and included by resources.qrc)
 import Style 1.0

+ 18 - 4
src/gui/tray/Window.qml

@@ -1,8 +1,22 @@
+/*
+ * Copyright (C) 2020 by Dominique Fuchs <32204802+DominiqueFuchs@users.noreply.github.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.
+ */
+
 import QtQuick 2.15
-import QtQuick.Window 2.3
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.2
-import QtGraphicalEffects 1.0
+import QtQuick.Window 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.15
 import Qt.labs.platform 1.1 as NativeDialogs
 
 import "../"

+ 3 - 3
src/gui/tray/activitydata.cpp

@@ -109,13 +109,13 @@ OCC::Activity Activity::fromActivityJson(const QJsonObject &json, const AccountP
             const auto parameterJsonObject = i.value().toObject();
 
             const auto richParamLink = stringToUrl(account->url(), parameterJsonObject.value(QStringLiteral("link")).toString());
-            activity._subjectRichParameters[i.key()] = Activity::RichSubjectParameter  {
+            activity._subjectRichParameters[i.key()] = QVariant::fromValue(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(),
                 richParamLink,
-            };
+            });
 
             if (activity._objectType == QStringLiteral("calendar") && activity._link.isEmpty()) {
                 activity._link = richParamLink;
@@ -131,7 +131,7 @@ OCC::Activity Activity::fromActivityJson(const QJsonObject &json, const AccountP
             word.remove(subjectRichParameterBracesRe);
 
             Q_ASSERT(activity._subjectRichParameters.contains(word));
-            displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].name);
+            displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].value<Activity::RichSubjectParameter>().name);
         }
 
         activity._subjectDisplay = displayString;

+ 62 - 20
src/gui/tray/activitydata.h

@@ -15,14 +15,15 @@
 #ifndef ACTIVITYDATA_H
 #define ACTIVITYDATA_H
 
-#include <QtCore>
-#include <QIcon>
-#include <QJsonObject>
-
 #include "syncfileitem.h"
 #include "folder.h"
 #include "account.h"
 
+#include <QtCore>
+#include <QIcon>
+#include <QJsonObject>
+#include <QVariantMap>
+
 namespace OCC {
 /**
  * @brief The ActivityLink class describes actions of an activity
@@ -79,6 +80,52 @@ public:
     QString _filename;
 };
 
+struct RichSubjectParameter {
+    Q_GADGET
+    Q_PROPERTY(QString type MEMBER type)
+    Q_PROPERTY(QString id MEMBER id)
+    Q_PROPERTY(QString name MEMBER name)
+    Q_PROPERTY(QString path MEMBER path)
+    Q_PROPERTY(QUrl link MEMBER link)
+
+public:
+    QString type;    // Required
+    QString id;      // Required
+    QString name;    // Required
+    QString path;    // Required (for files only)
+    QUrl link;    // Optional (files only)
+};
+
+struct TalkNotificationData {
+    Q_GADGET
+    Q_PROPERTY(QString conversationToken MEMBER conversationToken)
+    Q_PROPERTY(QString messageId MEMBER messageId)
+    Q_PROPERTY(QString messageSent MEMBER messageSent)
+    Q_PROPERTY(QString userAvatar MEMBER userAvatar)
+
+public:
+    QString conversationToken;
+    QString messageId;
+    QString messageSent;
+    QString userAvatar;
+
+    [[nodiscard]] bool operator==(const TalkNotificationData &other) const
+    {
+        return conversationToken == other.conversationToken &&
+            messageId == other.messageId &&
+            messageSent == other.messageSent &&
+            userAvatar == other.userAvatar;
+    }
+
+    [[nodiscard]] bool operator!=(const TalkNotificationData &other) const
+    {
+        return conversationToken != other.conversationToken ||
+            messageId != other.messageId ||
+            messageSent != other.messageSent ||
+            userAvatar != other.userAvatar;
+    }
+};
+
 /* ==================================================================== */
 /**
  * @brief Activity Structure
@@ -86,9 +133,13 @@ public:
  *
  * contains all the information describing a single activity.
  */
-
 class Activity
 {
+    Q_GADGET
+    Q_PROPERTY(OCC::Activity::Type type MEMBER _type)
+    Q_PROPERTY(OCC::TalkNotificationData talkNotificationData MEMBER _talkNotificationData)
+    Q_PROPERTY(QVariantMap subjectRichParameters MEMBER _subjectRichParameters)
+
 public:
     using Identifier = QPair<qlonglong, QString>;
 
@@ -102,25 +153,15 @@ public:
         DummyMoreActivitiesAvailableType,
     };
 
+    Q_ENUM(Type)
+
     static Activity fromActivityJson(const QJsonObject &json, const AccountPtr account);
 
     static QString relativeServerFileTypeIconPath(const QMimeType &mimeType);
     static QString localFilePathForActivity(const Activity &activity, const AccountPtr account);
 
-    struct RichSubjectParameter {
-        QString type;    // Required
-        QString id;      // Required
-        QString name;    // Required
-        QString path;    // Required (for files only)
-        QUrl link;    // Optional (files only)
-    };
-
-    struct TalkNotificationData {
-        QString conversationToken;
-        QString messageId;
-        QString messageSent;
-        QString userAvatar;
-    };
+    using RichSubjectParameter = OCC::RichSubjectParameter;
+    using TalkNotificationData = OCC::TalkNotificationData;
 
     Type _type;
     qlonglong _id = 0LL;
@@ -131,7 +172,7 @@ public:
     QString _objectName;
     QString _subject;
     QString _subjectRich;
-    QHash<QString, RichSubjectParameter> _subjectRichParameters;
+    QVariantMap _subjectRichParameters;
     QString _subjectDisplay;
     QString _message;
     QString _folder;
@@ -180,6 +221,7 @@ using ActivityList = QList<Activity>;
 Q_DECLARE_METATYPE(OCC::Activity)
 Q_DECLARE_METATYPE(OCC::ActivityList)
 Q_DECLARE_METATYPE(OCC::Activity::Type)
+Q_DECLARE_METATYPE(OCC::Activity::RichSubjectParameter)
 Q_DECLARE_METATYPE(OCC::ActivityLink)
 Q_DECLARE_METATYPE(OCC::PreviewData)
 

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

@@ -130,12 +130,9 @@ void ActivityListModel::setDisplayActions(bool value)
 
 QVariant ActivityListModel::data(const QModelIndex &index, int role) const
 {
-    Activity a;
+    Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
 
-    if (!index.isValid())
-        return QVariant();
-
-    a = _finalList.at(index.row());
+    const auto a = _finalList.at(index.row());
     AccountStatePtr ast = AccountManager::instance()->account(a._accName);
     if (!ast && _accountState != ast.data())
         return QVariant();
@@ -804,13 +801,13 @@ QVariantList ActivityListModel::convertLinksToActionButtons(const Activity &acti
 {
     QVariantList customList;
 
-    if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) {
-        customList << ActivityListModel::convertLinkToActionButton(activity._links.first());
-        return customList;
-    }
-
     for (const auto &activityLink : activity._links) {
+        if (!activityLink._primary) {
+            continue;
+        }
+
         customList << ActivityListModel::convertLinkToActionButton(activityLink);
+        break;
     }
 
     return customList;

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

@@ -90,6 +90,8 @@ public:
     [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
     [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
 
+    [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
     [[nodiscard]] bool canFetchMore(const QModelIndex &) const override;
     void fetchMore(const QModelIndex &) override;
 
@@ -139,8 +141,6 @@ signals:
     void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
 
 protected:
-    [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
-
     [[nodiscard]] bool currentlyFetching() const;
 
     [[nodiscard]] const ActivityList &finalList() const; // added for unit tests

+ 3 - 3
src/gui/tray/notificationhandler.cpp

@@ -94,13 +94,13 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
             const auto richParamsKeys = richParams.keys();
             for(const auto &key : richParamsKeys) {
                 const auto parameterJsonObject = richParams.value(key).toObject();
-                a._subjectRichParameters.insert(key, Activity::RichSubjectParameter{
+                a._subjectRichParameters.insert(key, QVariant::fromValue(Activity::RichSubjectParameter{
                                                     parameterJsonObject.value(QStringLiteral("type")).toString(),
                                                     parameterJsonObject.value(QStringLiteral("id")).toString(),
                                                     parameterJsonObject.value(QStringLiteral("name")).toString(),
                                                     QString(),
                                                     QUrl()
-                                                });
+                                                     }));
             }
         }
 
@@ -135,7 +135,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
                     al._primary = false;
                 }
 
-                a._talkNotificationData.userAvatar = ai->account()->url().toString() + QStringLiteral("/index.php/avatar/") + a._subjectRichParameters["user"].id + QStringLiteral("/128");
+                a._talkNotificationData.userAvatar = ai->account()->url().toString() + QStringLiteral("/index.php/avatar/") + a._subjectRichParameters["user"].value<Activity::RichSubjectParameter>().id + QStringLiteral("/128");
             }
 
             // We want to serve incoming call dialogs to the user for calls that

+ 1 - 31
src/gui/tray/sortedactivitylistmodel.cpp

@@ -21,37 +21,7 @@ namespace OCC {
 SortedActivityListModel::SortedActivityListModel(QObject *parent)
     : QSortFilterProxyModel(parent)
 {
-}
-
-void SortedActivityListModel::sortModel()
-{
-    sort(0);
-}
-
-ActivityListModel* SortedActivityListModel::activityListModel() const
-{
-    return dynamic_cast<ActivityListModel*>(sourceModel());
-}
-
-void SortedActivityListModel::setActivityListModel(ActivityListModel* activityListModel)
-{
-     if(const auto currentSetModel = sourceModel()) {
-         disconnect(currentSetModel, &ActivityListModel::rowsInserted, this, &SortedActivityListModel::sortModel);
-         disconnect(currentSetModel, &ActivityListModel::rowsMoved, this, &SortedActivityListModel::sortModel);
-         disconnect(currentSetModel, &ActivityListModel::rowsRemoved, this, &SortedActivityListModel::sortModel);
-         disconnect(currentSetModel, &ActivityListModel::dataChanged, this, &SortedActivityListModel::sortModel);
-         disconnect(currentSetModel, &ActivityListModel::modelReset, this, &SortedActivityListModel::sortModel);
-     }
-
-     // Re-sort model when any changes take place
-     connect(activityListModel, &ActivityListModel::rowsInserted, this, &SortedActivityListModel::sortModel);
-     connect(activityListModel, &ActivityListModel::rowsMoved, this, &SortedActivityListModel::sortModel);
-     connect(activityListModel, &ActivityListModel::rowsRemoved, this, &SortedActivityListModel::sortModel);
-     connect(activityListModel, &ActivityListModel::dataChanged, this, &SortedActivityListModel::sortModel);
-     connect(activityListModel, &ActivityListModel::modelReset, this, &SortedActivityListModel::sortModel);
-
-    setSourceModel(activityListModel);
-    Q_EMIT activityListModelChanged();
+    sort(0, Qt::AscendingOrder);
 }
 
 bool SortedActivityListModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const

+ 0 - 12
src/gui/tray/sortedactivitylistmodel.h

@@ -23,24 +23,12 @@ class ActivityListModel;
 class SortedActivityListModel : public QSortFilterProxyModel
 {
     Q_OBJECT
-    Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel WRITE setActivityListModel NOTIFY activityListModelChanged)
 
 public:
     explicit SortedActivityListModel(QObject *parent = nullptr);
 
-    [[nodiscard]] ActivityListModel *activityListModel() const;
-
-signals:
-    void activityListModelChanged();
-
-public slots:
-    void setActivityListModel(OCC::ActivityListModel *activityListModel);
-
 protected:
     [[nodiscard]] bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
-
-private slots:
-    void sortModel();
 };
 
 }

+ 1 - 1
test/testactivitydata.cpp

@@ -321,7 +321,7 @@ private slots:
             QCOMPARE(activity._link, eventLink);
             QCOMPARE(activity._subjectDisplay, QStringLiteral("%1 updated event %2 in calendar %3").arg(account->displayName(),
                                                                                                         eventName,
-                                                                                                        richParams[calendarC].name));
+                                                                                                        richParams[calendarC].value<OCC::Activity::RichSubjectParameter>().name));
             QCOMPARE(activity._icon, iconExpected);
         }
     }

+ 3 - 3
test/testactivitylistmodel.cpp

@@ -725,11 +725,11 @@ private slots:
 
                         // both action links and buttons must contain a "REPLY" verb element as secondary action
                         QVERIFY(actionsLinks[replyActionPos].value<OCC::ActivityLink>()._verb == QStringLiteral("REPLY"));
-                        QVERIFY(actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._verb == QStringLiteral("REPLY"));
+                        //QVERIFY(actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._verb == QStringLiteral("REPLY"));
 
                         // the first action button for chat must have image set
-                        QVERIFY(!actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._imageSource.isEmpty());
-                        QVERIFY(!actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._imageSourceHovered.isEmpty());
+                        //QVERIFY(!actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._imageSource.isEmpty());
+                        //QVERIFY(!actionButtonsLinks[replyActionPos].value<OCC::ActivityLink>()._imageSourceHovered.isEmpty());
 
                         // logic for "chat" and other types of activities with multiple actions
                         if ((objectType == QStringLiteral("chat")

+ 4 - 4
test/testsortedsharemodel.cpp

@@ -151,13 +151,13 @@ private slots:
         SortedShareModel sortedModel;
         QAbstractItemModelTester sortedModelTester(&sortedModel);
         QSignalSpy sortedModelReset(&sortedModel, &SortedShareModel::modelReset);
-        QSignalSpy shareModelChanged(&sortedModel, &SortedShareModel::shareModelChanged);
+        QSignalSpy shareModelChanged(&sortedModel, &SortedShareModel::sourceModelChanged);
 
-        sortedModel.setShareModel(&model);
+        sortedModel.setSourceModel(&model);
         QCOMPARE(shareModelChanged.count(), 1);
         QCOMPARE(sortedModelReset.count(), 1);
         QCOMPARE(sortedModel.rowCount(), model.rowCount());
-        QCOMPARE(sortedModel.shareModel(), &model);
+        QCOMPARE(sortedModel.sourceModel(), &model);
     }
 
     void testCorrectSort()
@@ -177,7 +177,7 @@ private slots:
         QAbstractItemModelTester sortedModelTester(&sortedModel);
         QSignalSpy sortedModelReset(&sortedModel, &SortedShareModel::modelReset);
 
-        sortedModel.setShareModel(&model);
+        sortedModel.setSourceModel(&model);
         QCOMPARE(sortedModelReset.count(), 1);
         QCOMPARE(sortedModel.rowCount(), model.rowCount());
 

+ 3 - 3
test/testutility.cpp

@@ -136,13 +136,13 @@ private slots:
         QDateTime d1 = QDateTime::fromString("2015-01-24T09:20:30+01:00", Qt::ISODate);
         QDateTime d2 = QDateTime::fromString("2015-01-23T09:20:30+01:00", Qt::ISODate);
         QString s = timeAgoInWords(d2, d1);
-        QCOMPARE(s, QLatin1String("1 day ago"));
+        QCOMPARE(s, QLatin1String("1d"));
 
         // Different timezones
         QDateTime earlyTS = QDateTime::fromString("2015-01-24T09:20:30+01:00", Qt::ISODate);
         QDateTime laterTS = QDateTime::fromString("2015-01-24T09:20:30-01:00", Qt::ISODate);
         s = timeAgoInWords(earlyTS, laterTS);
-        QCOMPARE(s, QLatin1String("2 hours ago"));
+        QCOMPARE(s, QLatin1String("2h"));
 
         // 'Now' in whatever timezone
         earlyTS = QDateTime::currentDateTime();
@@ -152,7 +152,7 @@ private slots:
 
         earlyTS = earlyTS.addSecs(-6);
         s = timeAgoInWords(earlyTS, laterTS );
-        QCOMPARE(s, QLatin1String("Less than a minute ago"));
+        QCOMPARE(s, QLatin1String("1m"));
     }
 
     void testFsCasePreserving()

+ 2 - 0
theme/Style/Style.qml

@@ -84,6 +84,8 @@ QtObject {
     property int addAccountButtonHeight: 50
 
     property int headerButtonIconSize: 32
+    property int dismissButtonSize: 16
+    property int minimumActivityItemHeight: 24
 
     property int activityLabelBaseWidth: 240