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
path: root/src
diff options
context:
space:
mode:
authoralex-z <blackslayer4@gmail.com>2022-01-04 17:28:26 +0300
committeralex-z <blackslayer4@gmail.com>2022-02-04 18:52:37 +0300
commitae44dd59787cd4614db1c11fe1376035345f36da (patch)
treef40cab03c5eca7410f7429bb5502ed09ab4255d7 /src
parent5ce8c9bf50f7a9e8753e0fddf471bbc8eb7d6ccc (diff)
Adjust icons for activity entries in main dialog. Refactor the dialog by splitting it to separate components.
Signed-off-by: alex-z <blackslayer4@gmail.com>
Diffstat (limited to 'src')
-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
16 files changed, 676 insertions, 351 deletions
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";
}