Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--resources.qrc5
-rw-r--r--src/gui/tray/ActivityActionButton.qml116
-rw-r--r--src/gui/tray/ActivityItem.qml298
-rw-r--r--src/gui/tray/ActivityItemActions.qml103
-rw-r--r--src/gui/tray/ActivityItemContent.qml127
-rw-r--r--src/gui/tray/ActivityItemContextMenu.qml25
-rw-r--r--src/gui/tray/ActivityList.qml17
-rw-r--r--src/gui/tray/CustomButton.qml61
-rw-r--r--src/gui/tray/CustomTextButton.qml49
-rw-r--r--src/gui/tray/FileActivityDialog.qml1
-rw-r--r--src/gui/tray/Window.qml2
-rw-r--r--src/gui/tray/activitydata.cpp11
-rw-r--r--src/gui/tray/activitydata.h12
-rw-r--r--src/gui/tray/activitylistmodel.cpp166
-rw-r--r--src/gui/tray/activitylistmodel.h29
-rw-r--r--src/gui/tray/notificationhandler.cpp9
-rw-r--r--src/gui/tray/usermodel.cpp1
-rw-r--r--test/testactivitylistmodel.cpp360
-rw-r--r--theme/Style/Style.qml6
19 files changed, 1011 insertions, 387 deletions
diff --git a/resources.qrc b/resources.qrc
index 43b277f77..a931e6716 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -23,5 +23,10 @@
<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>
diff --git a/src/gui/tray/ActivityActionButton.qml b/src/gui/tray/ActivityActionButton.qml
index 6cf3d3495..4c64e0bc8 100644
--- a/src/gui/tray/ActivityActionButton.qml
+++ b/src/gui/tray/ActivityActionButton.qml
@@ -1,109 +1,65 @@
-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()
+ }
}
}
diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml
index 598ae3b76..54c272cfb 100644
--- a/src/gui/tray/ActivityItem.qml
+++ b/src/gui/tray/ActivityItem.qml
@@ -1,264 +1,84 @@
-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
index 000000000..42875b9d3
--- /dev/null
+++ b/src/gui/tray/ActivityItemActions.qml
@@ -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
index 000000000..c5057d77d
--- /dev/null
+++ b/src/gui/tray/ActivityItemContent.qml
@@ -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
index 000000000..acf9b4267
--- /dev/null
+++ b/src/gui/tray/ActivityItemContextMenu.qml
@@ -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)
+ }
+ }
+}
diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml
index e1ab4f818..ced4ad2d6 100644
--- a/src/gui/tray/ActivityList.qml
+++ b/src/gui/tray/ActivityList.qml
@@ -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
index 000000000..7e73740bd
--- /dev/null
+++ b/src/gui/tray/CustomButton.qml
@@ -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
index 000000000..86323a1c0
--- /dev/null
+++ b/src/gui/tray/CustomTextButton.qml
@@ -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)
+ }
+}
diff --git a/src/gui/tray/FileActivityDialog.qml b/src/gui/tray/FileActivityDialog.qml
index 7fcd653d1..50452b0f3 100644
--- a/src/gui/tray/FileActivityDialog.qml
+++ b/src/gui/tray/FileActivityDialog.qml
@@ -15,6 +15,7 @@ Window {
height: 500
ActivityList {
+ isFileActivityList: true
anchors.fill: parent
model: dialog.model
}
diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml
index bf4ba5a9a..262e55920 100644
--- a/src/gui/tray/Window.qml
+++ b/src/gui/tray/Window.qml
@@ -748,7 +748,7 @@ Window {
openFileActivityDialog(displayPath, absolutePath)
}
onActivityItemClicked: {
- model.triggerDefaultAction(index)
+ model.slotTriggerDefaultAction(index)
}
}
diff --git a/src/gui/tray/activitydata.cpp b/src/gui/tray/activitydata.cpp
index 866c97956..16f1f1c6d 100644
--- a/src/gui/tray/activitydata.cpp
+++ b/src/gui/tray/activitydata.cpp
@@ -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;
+}
}
diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h
index 9b7c2ad98..31114e115 100644
--- a/src/gui/tray/activitydata.h
+++ b/src/gui/tray/activitydata.h
@@ -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;
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
index 8120612a5..ef0546f38 100644
--- a/src/gui/tray/activitylistmodel.cpp
+++ b/src/gui/tray/activitylistmodel.cpp
@@ -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;
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
index 66de2e498..34c591c2f 100644
--- a/src/gui/tray/activitylistmodel.h
+++ b/src/gui/tray/activitylistmodel.h
@@ -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;
};
}
diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp
index ccac7b1de..9acd325b9 100644
--- a/src/gui/tray/notificationhandler.cpp
+++ b/src/gui/tray/notificationhandler.cpp
@@ -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
diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp
index a3200df3b..44e9f1a0a 100644
--- a/src/gui/tray/usermodel.cpp
+++ b/src/gui/tray/usermodel.cpp
@@ -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";
}
diff --git a/test/testactivitylistmodel.cpp b/test/testactivitylistmodel.cpp
index 8faf21d64..64158822f 100644
--- a/test/testactivitylistmodel.cpp
+++ b/test/testactivitylistmodel.cpp
@@ -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)
diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml
index 421132b55..1425de189 100644
--- a/theme/Style/Style.qml
+++ b/theme/Style/Style.qml
@@ -50,6 +50,12 @@ QtObject {
property int headerButtonIconSize: 32
property int activityLabelBaseWidth: 240
+
+ property int activityItemActionPrimaryButtonMinWidth: 100
+ property int activityItemActionSecondaryButtonMinWidth: 80
+
+ property int roundButtonBackgroundVerticalMargins: 10
+ property int roundedButtonBackgroundVerticalMargins: 5
property int userStatusEmojiSize: 8
property int userStatusSpacing: 6