Adjust icons for activity entries in main dialog. Refactor the dialog by splitting...
authoralex-z <blackslayer4@gmail.com>
Tue, 4 Jan 2022 14:28:26 +0000 (16:28 +0200)
committeralex-z <blackslayer4@gmail.com>
Fri, 4 Feb 2022 15:52:37 +0000 (17:52 +0200)
Signed-off-by: alex-z <blackslayer4@gmail.com>
19 files changed:
resources.qrc
src/gui/tray/ActivityActionButton.qml
src/gui/tray/ActivityItem.qml
src/gui/tray/ActivityItemActions.qml [new file with mode: 0644]
src/gui/tray/ActivityItemContent.qml [new file with mode: 0644]
src/gui/tray/ActivityItemContextMenu.qml [new file with mode: 0644]
src/gui/tray/ActivityList.qml
src/gui/tray/CustomButton.qml [new file with mode: 0644]
src/gui/tray/CustomTextButton.qml [new file with mode: 0644]
src/gui/tray/FileActivityDialog.qml
src/gui/tray/Window.qml
src/gui/tray/activitydata.cpp
src/gui/tray/activitydata.h
src/gui/tray/activitylistmodel.cpp
src/gui/tray/activitylistmodel.h
src/gui/tray/notificationhandler.cpp
src/gui/tray/usermodel.cpp
test/testactivitylistmodel.cpp
theme/Style/Style.qml

index 43b277f77a5c33c1fba9220d1238c8ea35881358..a931e671662a7378a5bafd222bfc908c29deedb3 100644 (file)
         <file>src/gui/tray/UnifiedSearchResultListItem.qml</file>
         <file>src/gui/tray/UnifiedSearchResultNothingFound.qml</file>
         <file>src/gui/tray/UnifiedSearchResultSectionItem.qml</file>
+        <file>src/gui/tray/CustomButton.qml</file>
+        <file>src/gui/tray/CustomTextButton.qml</file>
+        <file>src/gui/tray/ActivityItemContextMenu.qml</file>
+        <file>src/gui/tray/ActivityItemActions.qml</file>
+        <file>src/gui/tray/ActivityItemContent.qml</file>
     </qresource>
 </RCC>
index 6cf3d34952b08a1551e34d3ed94d0960863188b9..4c64e0bc834ed9ff57fd3595f13a77a18ddea143 100644 (file)
-import QtQuick 2.5
+import QtQuick 2.15
 import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.15
 import Style 1.0
 
 Item {
     id: root
-    readonly property bool labelVisible: label.visible
-    readonly property bool iconVisible: icon.visible
 
-    // label value
     property string text: ""
-    
-    // font value
-    property var font: label.font
+    property string toolTipText: ""
 
-    // icon value
-    property string imageSource: ""
-
-    // Tooltip value
-    property string tooltipText: text
-
-    // text color
-    property color textColor: Style.ncTextColor
-    property color textColorHovered: Style.lightHover
-
-    // text background color
-    property color textBgColor: "transparent"
-    property color textBgColorHovered: Style.lightHover
+    property bool bold: false
 
-    // icon background color
-    property color iconBgColor: "transparent"
-    property color iconBgColorHovered: Style.lightHover
-
-    // text border color
-    property color textBorderColor: "transparent"
+    property string imageSource: ""
+    property string imageSourceHover: ""
 
-    property alias hovered: mouseArea.containsMouse
+    property color textColor: Style.unifiedSearchResulTitleColor
+    property color textColorHovered: Style.unifiedSearchResulSublineColor
 
     signal clicked()
 
-    Accessible.role: Accessible.Button
-    Accessible.name: text !== "" ? text : (tooltipText !== "" ? tooltipText : qsTr("Activity action button"))
-    Accessible.onPressAction: clicked()
-
-    // background with border around the Text
-    Rectangle {
-        visible: parent.labelVisible
+    Loader {
+        active: root.imageSource === ""
 
         anchors.fill: parent
 
-        // padding
-        anchors.topMargin: 10
-        anchors.bottomMargin: 10
-
-        border.color: parent.textBorderColor
-        border.width: 1
+        sourceComponent: CustomTextButton {
+             anchors.fill: parent
+             text: root.text
+             toolTipText: root.toolTipText
 
-        color: parent.hovered ? parent.textBgColorHovered : parent.textBgColor
+             textColor: root.textColor
+             textColorHovered: root.textColorHovered
 
-        radius: 25
+             onClicked: root.clicked()
+        }
     }
 
-    // background with border around the Image
-    Rectangle {
-        visible: parent.iconVisible
+    Loader {
+        active: root.imageSource !== ""
 
         anchors.fill: parent
 
-        color: parent.hovered ? parent.iconBgColorHovered : parent.iconBgColor
-    }
+        sourceComponent: CustomButton {
+            anchors.fill: parent
+            anchors.topMargin: Style.roundedButtonBackgroundVerticalMargins
+            anchors.bottomMargin: Style.roundedButtonBackgroundVerticalMargins
 
-    // label
-    Text {
-        id: label
-        visible: parent.text !== ""
-        text: parent.text
-        font: parent.font
-        color: parent.hovered ? parent.textColorHovered : parent.textColor
-        anchors.fill: parent
-        anchors.leftMargin: 10
-        anchors.rightMargin: 10
-        horizontalAlignment: Text.AlignHCenter
-        verticalAlignment: Text.AlignVCenter
-        elide: Text.ElideRight
-    }
+            text: root.text
+            toolTipText: root.toolTipText
 
-    // icon
-    Image {
-        id: icon
-        visible: parent.imageSource !== ""
-        anchors.centerIn: parent
-        source: parent.imageSource
-        sourceSize.width: visible ? 32 : 0
-        sourceSize.height: visible ? 32 : 0
-    }
+            textColor: root.textColor
+            textColorHovered: root.textColorHovered
 
-    MouseArea {
-        id: mouseArea
-        anchors.fill: parent
-        onClicked: parent.clicked()
-        hoverEnabled: true
-    }
+            bold: root.bold
+
+            imageSource: root.imageSource
+            imageSourceHover: root.imageSourceHover
+
+            bgColor: Style.ncBlue
 
-    ToolTip {
-        text: parent.tooltipText
-        delay: 1000
-        visible: text != "" && parent.hovered
+            onClicked: root.clicked()
+        }
     }
 }
index 598ae3b76b50ee9f842cdc0d969351c87c185bec..54c272cfba6f7d069b98d81af9b919da7b9ce7fe 100644 (file)
-import QtQml 2.12
-import QtQuick 2.9
-import QtQuick.Controls 2.2
-import QtQuick.Layouts 1.2
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
 import Style 1.0
 import com.nextcloud.desktopclient 1.0
 
 MouseArea {
-    id: activityMouseArea
+    id: root
 
-    readonly property int maxActionButtons: 2
     property Flickable flickable
 
+    property bool isFileActivityList: false
+
+    property bool isChatActivity: model.objectType === "chat" || model.objectType === "room"
+
     signal fileActivityButtonClicked(string absolutePath)
 
-    enabled: (path !== "" || link !== "")
+    enabled: (model.path !== "" || model.link !== "" || model.isCurrentUserFileActivity === true)
     hoverEnabled: true
 
+    height: childrenRect.height
+
+    ToolTip.visible: containsMouse && !activityContent.childHovered && model.displayLocation !== ""
+    ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
+    ToolTip.text: qsTr("In %1").arg(model.displayLocation)
+
+    Accessible.role: Accessible.ListItem
+    Accessible.name: (model.path !== "" && model.displayPath !== "") ? qsTr("Open %1 locally").arg(model.displayPath) : model.message
+    Accessible.onPressAction: root.clicked()
+
     Rectangle {
+        id: activityHover
         anchors.fill: parent
         color: (parent.containsMouse ? Style.lightHover : "transparent")
     }
 
-    ToolTip.visible: containsMouse && displayLocation !== ""
-    ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
-    ToolTip.text: qsTr("In %1").arg(displayLocation)
-        
-    RowLayout {
-        id: activityItem
-        
-        readonly property variant links: model.links
-        
-        readonly property int itemIndex: model.index
-        
-        width: activityMouseArea.width
-        height: Style.trayWindowHeaderHeight
+    ColumnLayout {
+        anchors.left: root.left
+        anchors.right: root.right
+        anchors.leftMargin: 15
+        anchors.rightMargin: 10
+
         spacing: 0
-        
-        Accessible.role: Accessible.ListItem
-        Accessible.name: path !== "" ? qsTr("Open %1 locally").arg(displayPath)
-                                     : message
-        Accessible.onPressAction: activityMouseArea.clicked()   
-        
-        Image {
-            id: activityIcon
-            Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
-            Layout.leftMargin: 20
-            Layout.preferredWidth: shareButton.icon.width
-            Layout.preferredHeight: shareButton.icon.height
-            verticalAlignment: Qt.AlignCenter
-            cache: true
-            source: icon
-            sourceSize.height: 64
-            sourceSize.width: 64
-        }
-        
-        Column {
-            id: activityTextColumn
-            Layout.leftMargin: 14
-            Layout.topMargin: 4
-            Layout.bottomMargin: 4
+
+        ActivityItemContent {
+            id: activityContent
+
             Layout.fillWidth: true
-            spacing: 4
-            Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-            
-            Text {
-                id: activityTextTitle
-                text: (type === "Activity" || type === "Notification") ? subject : message
-                width: parent.width
-                elide: Text.ElideRight
-                font.pixelSize: Style.topLinePixelSize
-                color: activityTextTitleColor
-            }
-            
-            Text {
-                id: activityTextInfo
-                text: (type === "Sync") ? displayPath
-                                        : (type === "File") ? subject
-                                                            : (type === "Notification") ? message
-                                                                                        : ""
-                height: (text === "") ? 0 : activityTextTitle.height
-                width: parent.width
-                elide: Text.ElideRight
-                font.pixelSize: Style.subLinePixelSize
-            }
-            
-            Text {
-                id: activityTextDateTime
-                text: dateTime
-                height: (text === "") ? 0 : activityTextTitle.height
-                width: parent.width
-                elide: Text.ElideRight
-                font.pixelSize: Style.subLinePixelSize
-                color: "#808080"
-            }
+
+            showDismissButton: model.links.length > 0 && model.linksForActionButtons.length === 0
+
+            activityData: model
+
+            Layout.preferredHeight: Style.trayWindowHeaderHeight
+
+            onShareButtonClicked: Systray.openShareDialog(model.displayPath, model.absolutePath)
+            onDismissButtonClicked: activityModel.slotTriggerDismiss(model.index)
         }
-        
-        RowLayout {
-            id: activityActionsLayout
-            spacing: 0
-            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
-            Layout.minimumWidth: 28
+
+        ActivityItemActions {
+            id: activityActions
+
+            visible: !root.isFileActivityList && model.linksForActionButtons.length > 0
+
+            Layout.preferredHeight: Style.trayWindowHeaderHeight * 0.85
             Layout.fillWidth: true
-            
-            function actionButtonIcon(actionIndex) {
-                const verb = String(model.links[actionIndex].verb);
-                if (verb === "WEB" && (model.objectType === "chat" || model.objectType === "call")) {
-                    return "qrc:///client/theme/reply.svg";
-                } else if (verb === "DELETE") {
-                    return "qrc:///client/theme/close.svg";
-                }
-                
-                return "qrc:///client/theme/confirm.svg";
-            }
-            
-            Repeater {
-                model: activityItem.links.length > maxActionButtons ? 1 : activityItem.links.length
-                
-                ActivityActionButton {
-                    id: activityActionButton
-                    
-                    readonly property int actionIndex: model.index
-                    readonly property bool primary: model.index === 0 && String(activityItem.links[actionIndex].verb) !== "DELETE"
-                    
-                    Layout.fillHeight: true                    
-                    
-                    text: !primary ? "" : activityItem.links[actionIndex].label
-                    
-                    imageSource: !primary ? activityActionsLayout.actionButtonIcon(actionIndex) : ""
-                    
-                    textColor: primary ? Style.ncBlue : "black"
-                    textColorHovered: Style.lightHover
-                    
-                    textBorderColor: Style.ncBlue
-                    
-                    textBgColor: "transparent"
-                    textBgColorHovered: Style.ncBlue
-                    
-                    tooltipText: activityItem.links[actionIndex].label
-                    
-                    Layout.minimumWidth: primary ? 80 : -1
-                    Layout.minimumHeight: parent.height
-                    
-                    Layout.preferredWidth: primary ? -1 : parent.height
-                    
-                    onClicked: activityModel.triggerAction(activityItem.itemIndex, actionIndex)
-                }
-                
-            }
+            Layout.leftMargin: 40
+            Layout.bottomMargin: model.links.length > 1 ? 5 : 0
+
+            displayActions: model.displayActions
+            objectType: model.objectType
+            linksForActionButtons: model.linksForActionButtons
+            linksContextMenu: model.linksContextMenu
+
+            moreActionsButtonColor: activityHover.color
+            maxActionButtons: activityModel.maxActionButtons
 
-            Button {
-                id: shareButton
-                
-                Layout.preferredWidth:  parent.height
-                Layout.fillHeight: true
-                Layout.alignment: Qt.AlignRight
-                flat: true
-                hoverEnabled: true
-                visible: isShareable
-                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
-                
-                Layout.preferredWidth: parent.height
-                Layout.preferredHeight: parent.height
-                Layout.alignment: Qt.AlignRight
-                
-                flat: true
-                hoverEnabled: true
-                visible: displayActions && ((path !== "") || (activityItem.links.length > maxActionButtons))
-                display: AbstractButton.IconOnly
-                icon.source: "qrc:///client/theme/more.svg"
-                icon.color: "transparent"
-                background: Rectangle {
-                    color: parent.hovered ? Style.lightHover : "transparent"
-                }
-                ToolTip.visible: hovered
-                ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
-                ToolTip.text: qsTr("Show more actions")
-                
-                Accessible.role: Accessible.Button
-                Accessible.name: qsTr("Show more actions")
-                Accessible.onPressAction: moreActionsButton.clicked()
-                
-                onClicked:  moreActionsButtonContextMenu.popup();
-                
-                Connections {
-                    target: flickable
-                    
-                    function onMovementStarted() {
-                        moreActionsButtonContextMenu.close();
-                    }
-                }
-                
-                Container {
-                    id: moreActionsButtonContextMenuContainer
-                    visible: moreActionsButtonContextMenu.opened
-                    
-                    width: moreActionsButtonContextMenu.width
-                    height: moreActionsButtonContextMenu.height
-                    anchors.right: moreActionsButton.right
-                    anchors.top: moreActionsButton.top
-                    
-                    AutoSizingMenu {
-                        id: moreActionsButtonContextMenu
-                        anchors.centerIn: parent
-                        
-                        // transform model to contain indexed actions with primary action filtered out
-                        function actionListToContextMenuList(actionList) {
-                            // early out with non-altered data
-                            if (activityItem.links.length <= maxActionButtons) {
-                                return actionList;
-                            }
-                            
-                            // add index to every action and filter 'primary' action out
-                            var reducedActionList = actionList.reduce(function(reduced, action, index) {
-                                if (!action.primary) {
-                                    var actionWithIndex = { actionIndex: index, label: action.label };
-                                    reduced.push(actionWithIndex);
-                                }
-                                return reduced;
-                            }, []);
-                            
-                            
-                            return reducedActionList;
-                        }
+            flickable: root.flickable
 
-                        MenuItem {
-                            text: qsTr("View activity")
-                            onClicked: fileActivityButtonClicked(absolutePath)
-                        }
-                        
-                        Repeater {
-                            id: moreActionsButtonContextMenuRepeater
-                            
-                            model: moreActionsButtonContextMenu.actionListToContextMenuList(activityItem.links)
-                            
-                            delegate: MenuItem {
-                                id: moreActionsButtonContextMenuEntry
-                                text: model.modelData.label
-                                onTriggered: activityModel.triggerAction(activityItem.itemIndex, model.modelData.actionIndex)
-                            }
-                        }
-                    }
-                }
-            }
+            onTriggerAction: activityModel.slotTriggerAction(model.index, actionIndex)
         }
     }
 }
diff --git a/src/gui/tray/ActivityItemActions.qml b/src/gui/tray/ActivityItemActions.qml
new file mode 100644 (file)
index 0000000..42875b9
--- /dev/null
@@ -0,0 +1,103 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import Style 1.0
+
+RowLayout {
+    id: root
+
+    spacing: 20
+
+    property string objectType: ""
+    property variant linksForActionButtons: []
+    property variant linksContextMenu: []
+    property bool displayActions: false
+
+    property color moreActionsButtonColor: "transparent"
+
+    property int maxActionButtons: 0
+
+    property Flickable flickable
+
+    signal triggerAction(int actionIndex)
+
+    Repeater {
+        id: actionsRepeater
+        // a max of maxActionButtons will get dispayed as separate buttons
+        model: root.linksForActionButtons
+
+        ActivityActionButton {
+            id: activityActionButton
+
+            readonly property bool primary: model.index === 0 && model.modelData.verb !== "DELETE"
+
+            Layout.minimumWidth: primary ? Style.activityItemActionPrimaryButtonMinWidth : Style.activityItemActionSecondaryButtonMinWidth
+            Layout.preferredHeight: primary ? parent.height : parent.height * 0.3
+            Layout.preferredWidth: primary ? -1 : parent.height
+
+            text: model.modelData.label
+            toolTipText: model.modelData.label
+
+            imageSource: model.modelData.imageSource
+            imageSourceHover: model.modelData.imageSourceHovered
+
+            textColor: imageSource !== "" ? Style.ncBlue : Style.unifiedSearchResulSublineColor
+            textColorHovered: imageSource !== "" ? Style.lightHover : Style.unifiedSearchResulTitleColor
+
+            bold: primary
+
+            onClicked: 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)
+
+        sourceComponent: Button {
+            id: moreActionsButton
+
+            icon.source: "qrc:///client/theme/more.svg"
+
+            background: Rectangle {
+                color: parent.hovered ? "white" : root.moreActionsButtonColor
+                radius: width / 2
+            }
+
+            ToolTip.visible: hovered
+            ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
+            ToolTip.text: qsTr("Show more actions")
+
+            Accessible.name: qsTr("Show more actions")
+
+            onClicked:  moreActionsButtonContextMenu.popup(moreActionsButton.x, moreActionsButton.y);
+
+            Connections {
+                target: root.flickable
+
+                function onMovementStarted() {
+                    moreActionsButtonContextMenu.close();
+                }
+            }
+
+            ActivityItemContextMenu {
+                id: moreActionsButtonContextMenu
+
+                maxActionButtons: root.maxActionButtons
+                linksContextMenu: root.linksContextMenu
+
+                onMenuEntryTriggered: function(entryIndex) {
+                    root.triggerAction(entryIndex)
+                }
+            }
+        }
+    }
+}
diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml
new file mode 100644 (file)
index 0000000..c5057d7
--- /dev/null
@@ -0,0 +1,127 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import Style 1.0
+import com.nextcloud.desktopclient 1.0
+
+RowLayout {
+    id: root
+
+    property variant activityData: {{}}
+
+    property color activityTextTitleColor: Style.ncTextColor
+
+    property bool showDismissButton: false
+
+    property bool childHovered: shareButton.hovered || dismissActionButton.hovered
+
+    signal dismissButtonClicked()
+    signal shareButtonClicked()
+
+    spacing: 10
+
+    Image {
+        id: activityIcon
+
+        Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+        Layout.preferredWidth: 32
+        Layout.preferredHeight: 32
+
+        verticalAlignment: Qt.AlignCenter
+        source: icon
+        sourceSize.height: 64
+        sourceSize.width: 64
+    }
+
+    Column {
+        id: activityTextColumn
+
+        Layout.topMargin: 4
+        Layout.fillWidth: true
+        Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+
+        spacing: 4
+
+        Label {
+            id: activityTextTitle
+            text: (root.activityData.type === "Activity" || root.activityData.type === "Notification") ? root.activityData.subject : root.activityData.message
+            width: parent.width
+            elide: Text.ElideRight
+            font.pixelSize: Style.topLinePixelSize
+            color: root.activityData.activityTextTitleColor
+        }
+
+        Label {
+            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 : activityTextTitle.height
+            width: parent.width
+            elide: Text.ElideRight
+            font.pixelSize: Style.subLinePixelSize
+        }
+
+        Label {
+            id: activityTextDateTime
+            text: root.activityData.dateTime
+            height: (text === "") ? 0 : activityTextTitle.height
+            width: parent.width
+            elide: Text.ElideRight
+            font.pixelSize: Style.subLinePixelSize
+            color: "#808080"
+        }
+    }
+
+    Button {
+        id: dismissActionButton
+
+        Layout.preferredWidth: parent.height * 0.40
+        Layout.preferredHeight: parent.height * 0.40
+
+        Layout.alignment: Qt.AlignCenter
+
+        Layout.margins: Style.roundButtonBackgroundVerticalMargins
+
+        ToolTip.visible: hovered
+        ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
+        ToolTip.text: qsTr("Dismiss")
+
+        Accessible.name: qsTr("Dismiss")
+
+        visible: root.showDismissButton && !shareButton.visible
+
+        background: Rectangle {
+            color: "transparent"
+        }
+
+        contentItem: Image {
+            anchors.fill: parent
+            source: parent.hovered ? "image://svgimage-custom-color/clear.svg/black" : "image://svgimage-custom-color/clear.svg/grey"
+            sourceSize.width: 24
+            sourceSize.height: 24
+        }
+
+        onClicked: root.dismissButtonClicked()
+    }
+
+    CustomButton {
+        id: shareButton
+
+        Layout.preferredWidth: parent.height * 0.70
+        Layout.preferredHeight: parent.height * 0.70
+
+        visible: root.activityData.isShareable
+
+        imageSource: "image://svgimage-custom-color/share.svg" + "/" + Style.ncBlue
+        imageSourceHover: "image://svgimage-custom-color/share.svg" + "/" + Style.ncTextColor
+
+        toolTipText: qsTr("Open share dialog")
+
+        bgColor: Style.ncBlue
+
+        onClicked: root.shareButtonClicked()
+    }
+}
diff --git a/src/gui/tray/ActivityItemContextMenu.qml b/src/gui/tray/ActivityItemContextMenu.qml
new file mode 100644 (file)
index 0000000..acf9b42
--- /dev/null
@@ -0,0 +1,25 @@
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+
+AutoSizingMenu {
+    id: moreActionsButtonContextMenu
+
+    property int maxActionButtons: 0
+
+    property var linksContextMenu: []
+
+    signal menuEntryTriggered(int index)
+
+    Repeater {
+        id: moreActionsButtonContextMenuRepeater
+
+        model: moreActionsButtonContextMenu.linksContextMenu
+
+        delegate: MenuItem {
+            id: moreActionsButtonContextMenuEntry
+            text: model.modelData.label
+            onTriggered: menuEntryTriggered(model.modelData.actionIndex)
+        }
+    }
+}
index e1ab4f81832238f9c7795a0576dd9524fed1c461..ced4ad2d61ab482caa5317a03f4a8494aa083d23 100644 (file)
@@ -1,14 +1,14 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 
-import Style 1.0
-
 import com.nextcloud.desktopclient 1.0 as NC
 
 ScrollView {
     id: controlRoot
     property alias model: activityList.model
 
+    property bool isFileActivityList: false
+
     signal showFileActivity(string displayPath, string absolutePath)
     signal activityItemClicked(int index)
 
@@ -31,12 +31,19 @@ ScrollView {
 
         clip: true
 
+        spacing: 10
+
         delegate: ActivityItem {
+            isFileActivityList: controlRoot.isFileActivityList
             width: activityList.contentWidth
-            height: Style.trayWindowHeaderHeight
             flickable: activityList
-            onClicked: activityItemClicked(model.index)
-            onFileActivityButtonClicked: showFileActivity(displayPath, absolutePath)
+            onClicked: {
+                if (model.isCurrentUserFileActivity) {
+                    showFileActivity(model.displayPath, model.absolutePath)
+                } else {
+                    activityItemClicked(model.index)
+                }
+            }
         }
     }
 }
diff --git a/src/gui/tray/CustomButton.qml b/src/gui/tray/CustomButton.qml
new file mode 100644 (file)
index 0000000..7e73740
--- /dev/null
@@ -0,0 +1,61 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+
+Button {
+    id: root
+
+    property string imageSource: ""
+    property string imageSourceHover: ""
+
+    property string toolTipText: ""
+
+    property color textColor
+    property color textColorHovered
+
+    property color bgColor: "transparent"
+
+    property bool bold: false
+
+    background: Rectangle {
+        color: root.bgColor
+        opacity: parent.hovered ? 1.0 : 0.3
+        radius: width / 2
+    }
+
+    leftPadding: root.text === "" ? 5 : 10
+    rightPadding: root.text === "" ? 5 : 10
+
+    contentItem: RowLayout {
+        Image {
+            id: icon
+
+            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+
+            source: root.hovered ? root.imageSourceHover : root.imageSource
+        }
+
+        Label {
+            Layout.maximumWidth: icon.width > 0 ? parent.width - icon.width - parent.spacing : parent.width
+            Layout.fillWidth: icon.status !== Image.Ready
+
+            text: root.text
+            font.bold: root.bold
+
+            visible: root.text !== ""
+
+            color: root.hovered ? root.textColorHovered : root.textColor
+
+            horizontalAlignment: Text.AlignHCenter
+            verticalAlignment: Text.AlignVCenter
+
+            elide: Text.ElideRight
+        }
+    }
+
+    ToolTip {
+        text: root.toolTipText
+        delay: Qt.styleHints.mousePressAndHoldInterval
+        visible: root.toolTipText !== "" && root.hovered
+    }
+}
diff --git a/src/gui/tray/CustomTextButton.qml b/src/gui/tray/CustomTextButton.qml
new file mode 100644 (file)
index 0000000..86323a1
--- /dev/null
@@ -0,0 +1,49 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.3
+import Style 1.0
+
+Label {
+    id: root
+
+    property string toolTipText: ""
+    property Action action: null
+    property alias acceptedButtons: mouseArea.acceptedButtons
+    property bool hovered: mouseArea.containsMouse
+
+    height: implicitHeight
+
+    property color textColor: Style.unifiedSearchResulTitleColor
+    property color textColorHovered: Style.unifiedSearchResulSublineColor
+
+    Accessible.role: Accessible.Button
+    Accessible.name: text
+    Accessible.onPressAction: root.clicked(null)
+
+    text: action ? action.text : ""
+    enabled: !action || action.enabled
+    onClicked: if (action) action.trigger()
+
+    font.underline: true
+    color: root.hovered ? root.textColorHovered : root.textColor
+    horizontalAlignment: Text.AlignLeft
+    verticalAlignment: Text.AlignVCenter
+    elide: Text.ElideRight
+
+    signal pressed(QtObject mouse)
+    signal clicked(QtObject mouse)
+
+    ToolTip {
+        text: root.toolTipText
+        delay: Qt.styleHints.mousePressAndHoldInterval
+        visible: root.toolTipText !== "" && root.hovered
+    }
+
+    MouseArea {
+        id: mouseArea
+        anchors.fill: parent
+        hoverEnabled: true
+
+        onClicked: root.clicked(mouse)
+        onPressed: root.pressed(mouse)
+    }
+}
index 7fcd653d1c10a91c4df1408aa8775fef1283cc59..50452b0f3cbaff374b70ce745aaf7b83dc6c4086 100644 (file)
@@ -15,6 +15,7 @@ Window {
     height: 500
 
     ActivityList {
+        isFileActivityList: true
         anchors.fill: parent
         model: dialog.model
     }
index bf4ba5a9afd71f86ce9d6230682f0a343135d6c1..262e55920f72c05a8a8843efc0a13c5bbe446edc 100644 (file)
@@ -748,7 +748,7 @@ Window {
                 openFileActivityDialog(displayPath, absolutePath)\r
             }\r
             onActivityItemClicked: {\r
-                model.triggerDefaultAction(index)\r
+                model.slotTriggerDefaultAction(index)\r
             }\r
         }\r
 \r
index 866c97956f9737cb74aac1288b12656756670eed..16f1f1c6d7b500a67ecb791c5f631489505abc1a 100644 (file)
@@ -33,4 +33,15 @@ Activity::Identifier Activity::ident() const
 {
     return Identifier(_id, _accName);
 }
+
+ActivityLink ActivityLink::createFomJsonObject(const QJsonObject &obj)
+{
+    ActivityLink activityLink;
+    activityLink._label = QUrl::fromPercentEncoding(obj.value(QStringLiteral("label")).toString().toUtf8());
+    activityLink._link = obj.value(QStringLiteral("link")).toString();
+    activityLink._verb = obj.value(QStringLiteral("type")).toString().toUtf8();
+    activityLink._primary = obj.value(QStringLiteral("primary")).toBool();
+
+    return activityLink;
+}
 }
index 9b7c2ad9804280e2baf77d6ddb7b5472e9f7e99f..31114e115f691a41c021fcc865fc9ff7b3f03456 100644 (file)
@@ -17,6 +17,7 @@
 
 #include <QtCore>
 #include <QIcon>
+#include <QJsonObject>
 
 namespace OCC {
 /**
@@ -28,13 +29,20 @@ namespace OCC {
 class ActivityLink
 {
     Q_GADGET
-
+    
+    Q_PROPERTY(QString imageSource MEMBER _imageSource)
+    Q_PROPERTY(QString imageSourceHovered MEMBER _imageSourceHovered)
     Q_PROPERTY(QString label MEMBER _label)
     Q_PROPERTY(QString link MEMBER _link)
     Q_PROPERTY(QByteArray verb MEMBER _verb)
     Q_PROPERTY(bool primary MEMBER _primary)
 
 public:
+    static ActivityLink createFomJsonObject(const QJsonObject &obj);
+
+public:
+    QString _imageSource;
+    QString _imageSourceHovered;
     QString _label;
     QString _link;
     QByteArray _verb;
@@ -80,11 +88,13 @@ public:
     QString _message;
     QString _folder;
     QString _file;
+    QString _renamedFile;
     QUrl _link;
     QDateTime _dateTime;
     qint64 _expireAtMsecs = -1;
     QString _accName;
     QString _icon;
+    bool _isCurrentUserFileActivity = false;
 
     // Stores information about the error
     int _status;
index 8120612a541646ef58810b15406aba7913bcfb2d..ef0546f38f9277f8112f25190c428f91bd7fe2b2 100644 (file)
@@ -54,7 +54,7 @@ ActivityListModel::ActivityListModel(AccountState *accountState,
 
 QHash<int, QByteArray> ActivityListModel::roleNames() const
 {
-    QHash<int, QByteArray> roles;
+    auto roles = QAbstractListModel::roleNames();
     roles[DisplayPathRole] = "displayPath";
     roles[PathRole] = "path";
     roles[AbsolutePathRole] = "absolutePath";
@@ -65,11 +65,14 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
     roles[ActionIconRole] = "icon";
     roles[ActionTextRole] = "subject";
     roles[ActionsLinksRole] = "links";
+    roles[ActionsLinksContextMenuRole] = "linksContextMenu";
+    roles[ActionsLinksForActionButtonsRole] = "linksForActionButtons";
     roles[ActionTextColorRole] = "activityTextTitleColor";
     roles[ObjectTypeRole] = "objectType";
     roles[PointInTimeRole] = "dateTime";
     roles[DisplayActions] = "displayActions";
     roles[ShareableRole] = "isShareable";
+    roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
     return roles;
 }
 
@@ -78,6 +81,11 @@ void ActivityListModel::setAccountState(AccountState *state)
     _accountState = state;
 }
 
+void ActivityListModel::setCurrentItem(const int currentItem)
+{
+    _currentItem = currentItem;
+}
+
 void ActivityListModel::setCurrentlyFetching(bool value)
 {
     _currentlyFetching = value;
@@ -116,10 +124,11 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         return QVariant();
 
     const auto getFilePath = [&]() {
-        if (!a._file.isEmpty()) {
+        const auto fileName = a._fileAction == QStringLiteral("file_renamed") ? a._renamedFile : a._file;
+        if (!fileName.isEmpty()) {
             const auto folder = FolderMan::instance()->folder(a._folder);
 
-            const QString relPath = folder ? folder->remotePath() + a._file : a._file;
+            const QString relPath = folder ? folder->remotePath() + fileName : fileName;
 
             const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
 
@@ -130,7 +139,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
             // If this is an E2EE file or folder, pretend we got no path, hiding the share button which is what we want
             if (folder) {
                 SyncJournalFileRecord rec;
-                folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
+                folder->journalDb()->getFileRecord(fileName.mid(1), &rec);
                 if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
                     return QString();
                 }
@@ -169,7 +178,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
     case DisplayPathRole:
         return getDisplayPath();
     case PathRole:
-        return QUrl::fromLocalFile(QFileInfo(getFilePath()).path());
+        return QFileInfo(getFilePath()).path();
     case AbsolutePathRole:
         return getFilePath();
     case DisplayLocationRole:
@@ -181,6 +190,15 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         }
         return customList;
     }
+
+    case ActionsLinksContextMenuRole: {
+        return ActivityListModel::convertLinksToMenuEntries(a);
+    }
+    
+    case ActionsLinksForActionButtonsRole: {
+        return ActivityListModel::convertLinksToActionButtons(a);
+    }
+
     case ActionIconRole: {
         if (a._type == Activity::NotificationType) {
             return "qrc:///client/theme/black/bell.svg";
@@ -249,7 +267,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         if (a._link.isEmpty()) {
             return "";
         } else {
-            return a._link;
+            return a._link.toString();
         }
     }
     case AccountRole:
@@ -262,7 +280,9 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
     case DisplayActions:
         return _displayActions;
     case ShareableRole:
-        return !data(index, PathRole).toString().isEmpty() && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
+        return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
+    case IsCurrentUserFileActivityRole:
+        return a._isCurrentUserFileActivity;
     default:
         return QVariant();
     }
@@ -310,6 +330,21 @@ void ActivityListModel::startFetchJob()
     job->start();
 }
 
+void ActivityListModel::setFinalList(const ActivityList &finalList)
+{
+    _finalList = finalList;
+}
+
+const ActivityList &ActivityListModel::finalList() const
+{
+    return _finalList;
+}
+
+int ActivityListModel::currentItem() const
+{
+    return _currentItem;
+}
+
 void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
 {
     auto activities = json.object().value("ocs").toObject().value("data").toArray();
@@ -333,6 +368,7 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
         auto json = activ.toObject();
 
         Activity a;
+        const auto activityUser = json.value(QStringLiteral("user")).toString();
         a._type = Activity::ActivityType;
         a._objectType = json.value(QStringLiteral("object_type")).toString();
         a._accName = ast->account()->displayName();
@@ -344,6 +380,7 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
         a._link = QUrl(json.value(QStringLiteral("link")).toString());
         a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
         a._icon = json.value(QStringLiteral("icon")).toString();
+        a._isCurrentUserFileActivity = a._objectType == QStringLiteral("files") && activityUser == ast->account()->davUser();
 
         auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
 
@@ -395,9 +432,9 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
 
     _activityLists.append(list);
 
-    emit activityJobStatusCode(statusCode);
-
     combineActivityLists();
+
+    emit activityJobStatusCode(statusCode);
 }
 
 void ActivityListModel::addErrorToActivityList(Activity activity)
@@ -486,7 +523,7 @@ void ActivityListModel::removeActivityFromActivityList(Activity activity)
     }
 }
 
-void ActivityListModel::triggerDefaultAction(int activityIndex)
+void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
 {
     if (activityIndex < 0 || activityIndex >= _finalList.size()) {
         qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size();
@@ -494,7 +531,7 @@ void ActivityListModel::triggerDefaultAction(int activityIndex)
     }
 
     const auto modelIndex = index(activityIndex);
-    const auto path = data(modelIndex, PathRole).toUrl();
+    const auto path = data(modelIndex, PathRole).toString();
 
     const auto activity = _finalList.at(activityIndex);
     if (activity._status == SyncFileItem::Conflict) {
@@ -544,15 +581,15 @@ void ActivityListModel::triggerDefaultAction(int activityIndex)
         return;
     }
 
-    if (path.isValid()) {
-        QDesktopServices::openUrl(path);
+    if (!path.isEmpty()) {
+        QDesktopServices::openUrl(QUrl::fromLocalFile(path));
     } else {
         const auto link = data(modelIndex, LinkRole).toUrl();
         Utility::openBrowser(link);
     }
 }
 
-void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
+void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex)
 {
     if (activityIndex < 0 || activityIndex >= _finalList.size()) {
         qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size();
@@ -576,11 +613,112 @@ void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
     emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
 }
 
+void ActivityListModel::slotTriggerDismiss(const int activityIndex)
+{
+    if (activityIndex < 0 || activityIndex >= _finalList.size()) {
+        qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size();
+        return;
+    }
+
+    const auto activityLinks = _finalList[activityIndex]._links;
+
+    const auto foundActivityLinkIt = std::find_if(std::cbegin(activityLinks), std::cend(activityLinks), [](const ActivityLink &link) {
+        return link._verb == QStringLiteral("DELETE");
+    });
+
+    if (foundActivityLinkIt == std::cend(activityLinks)) {
+        qCWarning(lcActivity) << "Couldn't find dismiss action in activity at index" << activityIndex
+                              << " links.size() " << activityLinks.size();
+        return;
+    }
+
+    const auto actionIndex = static_cast<int>(std::distance(activityLinks.begin(), foundActivityLinkIt));
+
+    if (actionIndex < 0 || actionIndex > activityLinks.size()) {
+        qCWarning(lcActivity) << "Couldn't find dismiss action in activity at index" << activityIndex
+                              << " actionIndex found " << actionIndex;
+        return;
+    }
+
+    slotTriggerAction(activityIndex, actionIndex);
+}
+
 AccountState *ActivityListModel::accountState() const
 {
     return _accountState;
 }
 
+QVariantList ActivityListModel::convertLinksToActionButtons(const Activity &activity)
+{
+    QVariantList customList;
+
+    if (activity._links.size() == 1) {
+        return customList;
+    }
+
+    if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) {
+        customList << ActivityListModel::convertLinkToActionButton(activity, activity._links.first());
+        return customList;
+    }
+
+    for (const auto &activityLink : activity._links) {
+        if (activityLink._verb == QStringLiteral("DELETE")
+            || (activity._objectType == QStringLiteral("chat") || activity._objectType == QStringLiteral("call")
+                || activity._objectType == QStringLiteral("room"))) {
+            customList << ActivityListModel::convertLinkToActionButton(activity, activityLink);
+        }
+    }
+
+    return customList;
+}
+
+QVariant ActivityListModel::convertLinkToActionButton(const OCC::Activity &activity, const OCC::ActivityLink &activityLink)
+{
+    auto activityLinkCopy = activityLink;
+
+    const auto isReplyIconApplicable = activityLink._verb == QStringLiteral("WEB")
+        && (activity._objectType == QStringLiteral("chat") || activity._objectType == QStringLiteral("call")
+            || activity._objectType == QStringLiteral("room"));
+
+    const QString replyButtonPath = QStringLiteral("image://svgimage-custom-color/reply.svg");
+
+    if (isReplyIconApplicable) {
+        activityLinkCopy._imageSource =
+            QString(replyButtonPath + "/" + OCC::Theme::instance()->wizardHeaderBackgroundColor().name());
+        activityLinkCopy._imageSourceHovered =
+            QString(replyButtonPath + "/" + OCC::Theme::instance()->wizardHeaderTitleColor().name());
+    }
+
+    const auto isReplyLabelApplicable = activityLink._verb == QStringLiteral("WEB")
+        && (activity._objectType == QStringLiteral("chat")
+        || (activity._objectType != QStringLiteral("room") && activity._objectType != QStringLiteral("call")));
+
+    if (activityLink._verb == QStringLiteral("DELETE")) {
+        activityLinkCopy._label = QObject::tr("Mark as read");
+    } else if (isReplyLabelApplicable) {
+        activityLinkCopy._label = QObject::tr("Reply");
+    }
+
+    return QVariant::fromValue(activityLinkCopy);
+}
+
+QVariantList ActivityListModel::convertLinksToMenuEntries(const Activity &activity)
+{
+    QVariantList customList;
+
+    if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) {
+        for (int i = 0; i < activity._links.size(); ++i) {
+            const auto &activityLink = activity._links[i];
+            if (!activityLink._primary) {
+                customList << QVariantMap{
+                    {QStringLiteral("actionIndex"), i}, {QStringLiteral("label"), activityLink._label}};
+            }
+        }
+    }
+
+    return customList;
+}
+
 void ActivityListModel::combineActivityLists()
 {
     ActivityList resultList;
index 66de2e4986df565f5068fb1d6d78d5e5ddd75342..34c591c2f7692cdea8814d93742136f49a423b6c 100644 (file)
@@ -40,6 +40,8 @@ class ActivityListModel : public QAbstractListModel
 {
     Q_OBJECT
 
+    Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT)
+
     Q_PROPERTY(AccountState *accountState READ accountState CONSTANT)
 public:
     enum DataRole {
@@ -47,6 +49,8 @@ public:
         AccountRole,
         ObjectTypeRole,
         ActionsLinksRole,
+        ActionsLinksContextMenuRole,
+        ActionsLinksForActionButtonsRole,
         ActionTextRole,
         ActionTextColorRole,
         ActionRole,
@@ -60,6 +64,7 @@ public:
         AccountConnectedRole,
         DisplayActions,
         ShareableRole,
+        IsCurrentUserFileActivityRole,
     };
     Q_ENUM(DataRole)
 
@@ -84,15 +89,22 @@ public:
     void removeActivityFromActivityList(int row);
     void removeActivityFromActivityList(Activity activity);
 
-    Q_INVOKABLE void triggerDefaultAction(int activityIndex);
-    Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
-
     AccountState *accountState() const;
     void setAccountState(AccountState *state);
 
+    static constexpr quint32 maxActionButtons()
+    {
+        return MaxActionButtons;
+    }
+
+    void setCurrentItem(const int currentItem);
+
 public slots:
     void slotRefreshActivity();
     void slotRemoveAccount();
+    void slotTriggerDefaultAction(const int activityIndex);
+    void slotTriggerAction(const int activityIndex, const int actionIndex);
+    void slotTriggerDismiss(const int activityIndex);
 
 signals:
     void activityJobStatusCode(int statusCode);
@@ -110,7 +122,16 @@ protected:
 
     virtual void startFetchJob();
 
+    // added these for unit tests
+    void setFinalList(const ActivityList &finalList);
+    const ActivityList &finalList() const;
+    int currentItem() const;
+    //
+
 private:
+    static QVariantList convertLinksToMenuEntries(const Activity &activity);
+    static QVariantList convertLinksToActionButtons(const Activity &activity);
+    static QVariant convertLinkToActionButton(const Activity &activity, const ActivityLink &activityLink);
     void combineActivityLists();
     bool canFetchActivities() const;
 
@@ -137,6 +158,8 @@ private:
     bool _currentlyFetching = false;
     bool _doneFetching = false;
     bool _hideOldActivities = true;
+
+    static constexpr quint32 MaxActionButtons = 2;
 };
 }
 
index ccac7b1de72641b226225f1706d085501faa8792..9acd325b92fb1e85ad3ee5684174ee0fa304a47d 100644 (file)
@@ -121,14 +121,7 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
 
         auto actions = json.value("actions").toArray();
         foreach (auto action, actions) {
-            auto actionJson = action.toObject();
-            ActivityLink al;
-            al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
-            al._link = actionJson.value("link").toString();
-            al._verb = actionJson.value("type").toString().toUtf8();
-            al._primary = actionJson.value("primary").toBool();
-
-            a._links.append(al);
+            a._links.append(ActivityLink::createFomJsonObject(action.toObject()));
         }
 
         // Add another action to dismiss notification on server
index a3200df3beab23b8af538543fd3a9d785b665195..44e9f1a0a9d299054c1ab9c5ca5ff81310a0f77c 100644 (file)
@@ -514,6 +514,7 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
         activity._fileAction = "file_created";
     } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
         activity._fileAction = "file_renamed";
+        activity._renamedFile = item->_renameTarget;
     } else {
         activity._fileAction = "file_changed";
     }
index 8faf21d64894e202fc90c9ca92be6e0cdea48538..64158822fe9ad4ac4c1d343037be68779400a105 100644 (file)
@@ -25,7 +25,9 @@
 #include <QSignalSpy>
 #include <QTest>
 
+namespace {
 constexpr auto startingId = 90000;
+}
 
 static QByteArray fake404Response = R"(
 {"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}}
@@ -39,28 +41,6 @@ static QByteArray fake500Response = R"(
 {"ocs":{"meta":{"status":"failure","statuscode":500,"message":"Internal Server Error.\n"},"data":[]}}
 )";
 
-class TestingALM : public OCC::ActivityListModel
-{
-    Q_OBJECT
-
-public:
-    TestingALM() = default;
-
-    void startFetchJob() override
-    {
-        auto *job = new OCC::JsonApiJob(accountState()->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
-        QObject::connect(job, &OCC::JsonApiJob::jsonReceived,
-            this, &TestingALM::activitiesReceived);
-
-        QUrlQuery params;
-        params.addQueryItem(QLatin1String("since"), QString::number(startingId));
-        params.addQueryItem(QLatin1String("limit"), QString::number(50));
-        job->addQueryParams(params);
-
-        job->start();
-    };
-};
-
 class FakeRemoteActivityStorage
 {
     FakeRemoteActivityStorage() = default;
@@ -101,8 +81,6 @@ public:
     {
         // Insert activity data
         for (quint32 i = 0; i <= _numItemsToInsert; i++) {
-            _startingId++;
-
             QJsonObject activity;
             activity.insert(QStringLiteral("object_type"), "files");
             activity.insert(QStringLiteral("activity_id"), _startingId);
@@ -114,35 +92,184 @@ public:
             activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/apps/files/img/add-color.svg"));
 
             _activityData.push_back(activity);
+
+            _startingId++;
         }
 
         // Insert notification data
         for (quint32 i = 0; i < _numItemsToInsert; i++) {
-            _startingId++;
             QJsonObject activity;
             activity.insert(QStringLiteral("activity_id"), _startingId);
             activity.insert(QStringLiteral("object_type"), "calendar");
             activity.insert(QStringLiteral("type"), QStringLiteral("calendar-event"));
-            activity.insert(QStringLiteral("subject"), QStringLiteral("You created event %1 in calendar Events").arg(i));
+            activity.insert(
+                QStringLiteral("subject"), QStringLiteral("You created event %1 in calendar Events").arg(i));
             activity.insert(QStringLiteral("message"), QStringLiteral(""));
             activity.insert(QStringLiteral("object_name"), QStringLiteral(""));
             activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate));
             activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/calendar.svg"));
 
+            QJsonArray actionsArray;
+
+            QJsonObject secondaryAction;
+            secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss"));
+            secondaryAction.insert(QStringLiteral("link"),
+                QString(QStringLiteral("http://cloud.example.de/remote.php/dav")
+                    + QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications") + QString::number(i)));
+            secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE"));
+            secondaryAction.insert(QStringLiteral("primary"), false);
+            actionsArray.push_back(secondaryAction);
+
+            _activityData.push_back(activity);
+
+            _startingId++;
+        }
+
+        // Insert notification data
+        for (quint32 i = 0; i < _numItemsToInsert; i++) {
+            QJsonObject activity;
+            activity.insert(QStringLiteral("activity_id"), _startingId);
+            activity.insert(QStringLiteral("object_type"), "chat");
+            activity.insert(QStringLiteral("type"), QStringLiteral("chat"));
+            activity.insert(QStringLiteral("subject"), QStringLiteral("You have received %1's message").arg(i));
+            activity.insert(QStringLiteral("message"), QStringLiteral(""));
+            activity.insert(QStringLiteral("object_name"), QStringLiteral(""));
+            activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate));
+            activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg"));
+
+            QJsonArray actionsArray;
+
+            QJsonObject primaryAction;
+            primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat"));
+            primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd"));
+            primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB"));
+            primaryAction.insert(QStringLiteral("primary"), true);
+            actionsArray.push_back(primaryAction);
+
+            QJsonObject secondaryAction;
+            secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss"));
+            secondaryAction.insert(QStringLiteral("link"),
+                QString(QStringLiteral("http://cloud.example.de/remote.php/dav")
+                    + QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications") + QString::number(i)));
+            secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE"));
+            secondaryAction.insert(QStringLiteral("primary"), false);
+            actionsArray.push_back(secondaryAction);
+
+            QJsonObject additionalAction;
+            additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 1"));
+            additionalAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd"));
+            additionalAction.insert(QStringLiteral("type"), QStringLiteral("POST"));
+            additionalAction.insert(QStringLiteral("primary"), false);
+            actionsArray.push_back(additionalAction);
+            additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 2"));
+            actionsArray.push_back(additionalAction);
+
+            activity.insert(QStringLiteral("actions"), actionsArray);
+
+            _activityData.push_back(activity);
+
+            _startingId++;
+        }
+
+        // Insert notification data
+        for (quint32 i = 0; i < _numItemsToInsert; i++) {
+            QJsonObject activity;
+            activity.insert(QStringLiteral("activity_id"), _startingId);
+            activity.insert(QStringLiteral("object_type"), "room");
+            activity.insert(QStringLiteral("type"), QStringLiteral("room"));
+            activity.insert(QStringLiteral("subject"), QStringLiteral("You have been invited into room%1").arg(i));
+            activity.insert(QStringLiteral("message"), QStringLiteral(""));
+            activity.insert(QStringLiteral("object_name"), QStringLiteral(""));
+            activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate));
+            activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg"));
+
+            QJsonArray actionsArray;
+
+            QJsonObject primaryAction;
+            primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat"));
+            primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd"));
+            primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB"));
+            primaryAction.insert(QStringLiteral("primary"), true);
+            actionsArray.push_back(primaryAction);
+
+            QJsonObject secondaryAction;
+            secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss"));
+            secondaryAction.insert(QStringLiteral("link"),
+                QString(QStringLiteral("http://cloud.example.de/remote.php/dav")
+                    + QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications") + QString::number(i)));
+            secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE"));
+            secondaryAction.insert(QStringLiteral("primary"), false);
+            actionsArray.push_back(secondaryAction);
+
+            activity.insert(QStringLiteral("actions"), actionsArray);
+
             _activityData.push_back(activity);
+
+            _startingId++;
         }
+
+        // Insert notification data
+        for (quint32 i = 0; i < _numItemsToInsert; i++) {
+            QJsonObject activity;
+            activity.insert(QStringLiteral("activity_id"), _startingId);
+            activity.insert(QStringLiteral("object_type"), "call");
+            activity.insert(QStringLiteral("type"), QStringLiteral("call"));
+            activity.insert(QStringLiteral("subject"), QStringLiteral("You have missed a %1's call").arg(i));
+            activity.insert(QStringLiteral("message"), QStringLiteral(""));
+            activity.insert(QStringLiteral("object_name"), QStringLiteral(""));
+            activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate));
+            activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg"));
+
+            QJsonArray actionsArray;
+
+            QJsonObject primaryAction;
+            primaryAction.insert(QStringLiteral("label"), QStringLiteral("Call back"));
+            primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd"));
+            primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB"));
+            primaryAction.insert(QStringLiteral("primary"), true);
+            actionsArray.push_back(primaryAction);
+
+            QJsonObject secondaryAction;
+            secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss"));
+            secondaryAction.insert(QStringLiteral("link"),
+                QString(QStringLiteral("http://cloud.example.de/remote.php/dav")
+                    + QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications") + QString::number(i)));
+            secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE"));
+            secondaryAction.insert(QStringLiteral("primary"), false);
+            actionsArray.push_back(secondaryAction);
+
+            activity.insert(QStringLiteral("actions"), actionsArray);
+
+            _activityData.push_back(activity);
+
+            _startingId++;
+        }
+
+        _startingId--;
     }
 
     const QByteArray activityJsonData(int sinceId, int limit)
     {
         QJsonArray data;
 
-        for(int dataIndex = _activityData.size() - 1, iteration = 0;
-            dataIndex > 0 && iteration < limit;
-            --dataIndex, ++iteration) {
-
-            if(_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt() > sinceId) {
-                data.append(_activityData[dataIndex]);
+        const auto itFound = std::find_if(
+            std::cbegin(_activityData), std::cend(_activityData), [&sinceId](const QJsonValue &currentActivityValue) {
+                const auto currentActivityId =
+                    currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt();
+                return currentActivityId == sinceId;
+            });
+
+        const int startIndex = itFound != std::cend(_activityData)
+            ? static_cast<int>(std::distance(std::cbegin(_activityData), itFound))
+            : -1;
+
+        if (startIndex > 0) {
+            for (int dataIndex = startIndex, iteration = 0; dataIndex >= 0 && iteration < limit;
+                 --dataIndex, ++iteration) {
+                if (_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt()
+                    > sinceId - limit) {
+                    data.append(_activityData[dataIndex]);
+                }
             }
         }
 
@@ -154,6 +281,24 @@ public:
         return QJsonDocument(root).toJson();
     }
 
+    QJsonValue activityById(int id)
+    {
+        const auto itFound = std::find_if(
+            std::cbegin(_activityData), std::cend(_activityData), [&id](const QJsonValue &currentActivityValue) {
+                const auto currentActivityId =
+                    currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt();
+                return currentActivityId == id;
+            });
+
+        if (itFound != std::cend(_activityData)) {
+            return (*itFound);
+        }
+
+        return {};
+    }
+
+    int startingIdLast() const { return _startingId; }
+
 private:
     static FakeRemoteActivityStorage *_instance;
     QJsonArray _activityData;
@@ -164,6 +309,64 @@ private:
 
 FakeRemoteActivityStorage *FakeRemoteActivityStorage::_instance = nullptr;
 
+class TestingALM : public OCC::ActivityListModel
+{
+    Q_OBJECT
+
+public:
+    TestingALM() = default;
+
+    void startFetchJob() override
+    {
+        auto *job = new OCC::JsonApiJob(
+            accountState()->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
+        QObject::connect(this, &TestingALM::activityJobStatusCode, this, &TestingALM::slotProcessReceivedActivities);
+        QObject::connect(job, &OCC::JsonApiJob::jsonReceived, this, &TestingALM::activitiesReceived);
+
+        QUrlQuery params;
+        params.addQueryItem(QLatin1String("since"), QString::number(currentItem()));
+        params.addQueryItem(QLatin1String("limit"), QString::number(50));
+        job->addQueryParams(params);
+
+        job->start();
+    }
+
+public slots:
+    void slotProcessReceivedActivities()
+    {
+        if (rowCount() > _numRowsPrev) {
+            auto finalListCopy = finalList();
+            for (int i = _numRowsPrev; i < rowCount(); ++i) {
+                const auto modelIndex = index(i, 0);
+                auto activity = finalListCopy.at(modelIndex.row());
+                if (activity._links.isEmpty()) {
+                    const auto activityJsonObject = FakeRemoteActivityStorage::instance()->activityById(activity._id);
+
+                    if (!activityJsonObject.isNull()) {
+                        // because "_links" are normally populated within the notificationhandler.cpp, which we don't run as part of this unit test, we have to fill them here
+                        // TODO: move the logic to populate "_links" to "activitylistmodel.cpp"
+                        auto actions = activityJsonObject.toObject().value("actions").toArray();
+                        foreach (auto action, actions) {
+                            activity._links.append(OCC::ActivityLink::createFomJsonObject(action.toObject()));
+                        }
+
+                        finalListCopy[modelIndex.row()] = activity;
+                    }
+                }
+            }
+
+            setFinalList(finalListCopy);
+        }
+        _numRowsPrev = rowCount();
+        emit activitiesProcessed();
+    }
+signals:
+    void activitiesProcessed();
+
+private:
+    int _numRowsPrev = 0;
+};
+
 class TestActivityListModel : public QObject
 {
     Q_OBJECT
@@ -238,8 +441,9 @@ private slots:
 
         QCOMPARE(model.rowCount(), 0);
 
+        model.setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast());
         model.startFetchJob();
-        QSignalSpy activitiesJob(&model, &TestingALM::activityJobStatusCode);
+        QSignalSpy activitiesJob(&model, &TestingALM::activitiesProcessed);
         QVERIFY(activitiesJob.wait(3000));
         QCOMPARE(model.rowCount(), 50);
     };
@@ -348,8 +552,9 @@ private slots:
 
         QCOMPARE(model.rowCount(), 0);
 
+        model.setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast());
         model.startFetchJob();
-        QSignalSpy activitiesJob(&model, &TestingALM::activityJobStatusCode);
+        QSignalSpy activitiesJob(&model, &TestingALM::activitiesProcessed);
         QVERIFY(activitiesJob.wait(3000));
         QCOMPARE(model.rowCount(), 50);
 
@@ -385,8 +590,10 @@ private slots:
         for (int i = 0; i < model.rowCount(); i++) {
             const auto index = model.index(i, 0);
 
-            QVERIFY(index.data(OCC::ActivityListModel::ObjectTypeRole).canConvert<int>());
-            const auto type = index.data(OCC::ActivityListModel::ObjectTypeRole).toInt();
+            auto text = index.data(OCC::ActivityListModel::ActionTextRole).toString();
+
+            QVERIFY(index.data(OCC::ActivityListModel::ActionRole).canConvert<int>());
+            const auto type = index.data(OCC::ActivityListModel::ActionRole).toInt();
             QVERIFY(type >= OCC::Activity::ActivityType);
 
             QVERIFY(!index.data(OCC::ActivityListModel::ObjectTypeRole).toInt());
@@ -407,6 +614,87 @@ private slots:
         }
     };
 
+    void tesActivityActionstData()
+    {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+        model.setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast());
+
+        int prevModelRowCount = model.rowCount();
+
+        do {
+            prevModelRowCount = model.rowCount();
+            model.startFetchJob();
+            QSignalSpy activitiesJob(&model, &TestingALM::activitiesProcessed);
+            QVERIFY(activitiesJob.wait(3000));
+
+
+            for (int i = prevModelRowCount; i < model.rowCount(); i++) {
+                const auto index = model.index(i, 0);
+
+                const auto actionsLinks = index.data(OCC::ActivityListModel::ActionsLinksRole).toList();
+                if (!actionsLinks.isEmpty()) {
+                    const auto actionsLinksContextMenu =
+                        index.data(OCC::ActivityListModel::ActionsLinksContextMenuRole).toList();
+
+                    // context menu must be shorter than total action links
+                    QVERIFY(actionsLinks.isEmpty() || actionsLinksContextMenu.size() < actionsLinks.size());
+
+                    // context menu must not contain the primary action
+                    QVERIFY(std::find_if(std::begin(actionsLinksContextMenu), std::end(actionsLinksContextMenu),
+                                [](const QVariant &entry) { return entry.value<OCC::ActivityLink>()._primary; })
+                        == std::end(actionsLinksContextMenu));
+
+                    const auto objectType = index.data(OCC::ActivityListModel::ObjectTypeRole).toString();
+
+                    if ((objectType == QStringLiteral("chat") || objectType == QStringLiteral("call")
+                            || objectType == QStringLiteral("room"))) {
+                        const auto actionButtonsLinks =
+                            index.data(OCC::ActivityListModel::ActionsLinksForActionButtonsRole).toList();
+
+                        // both action links and buttons must contain a "WEB" verb element at the beginning
+                        QVERIFY(actionsLinks[0].value<OCC::ActivityLink>()._verb == QStringLiteral("WEB"));
+                        QVERIFY(actionButtonsLinks[0].value<OCC::ActivityLink>()._verb == QStringLiteral("WEB"));
+                        
+                        // the first action button for chat must have image set
+                        QVERIFY(!actionButtonsLinks[0].value<OCC::ActivityLink>()._imageSource.isEmpty());
+                        QVERIFY(!actionButtonsLinks[0].value<OCC::ActivityLink>()._imageSourceHovered.isEmpty());
+
+                        // logic for "chat" and other types of activities with multiple actions
+                        if ((objectType == QStringLiteral("chat")
+                                || (objectType != QStringLiteral("room") && objectType != QStringLiteral("call")))) {
+
+                            // button's label for "chat" must be renamed to "Reply"
+                            QVERIFY(actionButtonsLinks[0].value<OCC::ActivityLink>()._label == QObject::tr("Reply"));
+
+                            if (static_cast<quint32>(actionsLinks.size()) > OCC::ActivityListModel::maxActionButtons()) {
+                                // in case total actions is longer than ActivityListModel::maxActionButtons, only one button must be present in a list of action buttons
+                                QVERIFY(actionButtonsLinks.size() == 1);
+                                const auto actionButtonsAndContextMenuEntries = actionButtonsLinks + actionsLinksContextMenu;
+                                // in case total actions is longer than ActivityListModel::maxActionButtons, then a sum of action buttons and action menu entries must be equal to a total of action links
+                                QVERIFY(actionButtonsLinks.size() + actionsLinksContextMenu.size() == actionsLinks.size());
+                            } else {
+                                // in case a total of actions is less or equal to than ActivityListModel::maxActionButtons, then the length of action buttons must be greater than 1 and should contain "Mark as read" button at the end
+                                QVERIFY(actionButtonsLinks.size() > 1);
+                                QVERIFY(actionButtonsLinks[1].value<OCC::ActivityLink>()._label
+                                    == QObject::tr("Mark as read"));
+                            }
+                        } else if ((objectType == QStringLiteral("call"))) {
+                            QVERIFY(
+                                actionButtonsLinks[0].value<OCC::ActivityLink>()._label == QStringLiteral("Call back"));
+                        }
+                    } else {
+                        QVERIFY(actionsLinks[0].value<OCC::ActivityLink>()._label == QStringLiteral("Dismiss"));
+                    }
+                }
+            }
+
+        } while (prevModelRowCount < model.rowCount());
+    };
+
 };
 
 QTEST_MAIN(TestActivityListModel)
index 421132b557942d8d61ec8c21af0dafba399672b8..1425de189ccb4fe7c2cca467b8421c1893374abc 100644 (file)
@@ -50,6 +50,12 @@ QtObject {
     property int headerButtonIconSize: 32\r
 \r
     property int activityLabelBaseWidth: 240\r
+\r
+    property int activityItemActionPrimaryButtonMinWidth: 100\r
+    property int activityItemActionSecondaryButtonMinWidth: 80\r
+\r
+    property int roundButtonBackgroundVerticalMargins: 10\r
+    property int roundedButtonBackgroundVerticalMargins: 5\r
     \r
     property int userStatusEmojiSize: 8\r
     property int userStatusSpacing: 6\r