mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 23:28:14 +03:00
Merge pull request #4186 from nextcloud/feature/improve-activity-buttons
Feature/improve activity buttons
This commit is contained in:
commit
e5cae81f25
19 changed files with 1013 additions and 389 deletions
|
@ -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>
|
||||
|
|
|
@ -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: ""
|
||||
|
||||
property bool bold: false
|
||||
|
||||
// icon value
|
||||
property string imageSource: ""
|
||||
property string imageSourceHover: ""
|
||||
|
||||
// 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
|
||||
|
||||
// icon background color
|
||||
property color iconBgColor: "transparent"
|
||||
property color iconBgColorHovered: Style.lightHover
|
||||
|
||||
// text border color
|
||||
property color textBorderColor: "transparent"
|
||||
|
||||
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
|
||||
sourceComponent: CustomTextButton {
|
||||
anchors.fill: parent
|
||||
text: root.text
|
||||
toolTipText: root.toolTipText
|
||||
|
||||
border.color: parent.textBorderColor
|
||||
border.width: 1
|
||||
textColor: root.textColor
|
||||
textColorHovered: root.textColorHovered
|
||||
|
||||
color: parent.hovered ? parent.textBgColorHovered : parent.textBgColor
|
||||
|
||||
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
|
||||
|
||||
ToolTip {
|
||||
text: parent.tooltipText
|
||||
delay: 1000
|
||||
visible: text != "" && parent.hovered
|
||||
imageSource: root.imageSource
|
||||
imageSourceHover: root.imageSourceHover
|
||||
|
||||
bgColor: Style.ncBlue
|
||||
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: activityActionsLayout
|
||||
spacing: 0
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.minimumWidth: 28
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
ActivityItemContent {
|
||||
id: activityContent
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
ActivityItemActions {
|
||||
id: activityActions
|
||||
|
||||
visible: !root.isFileActivityList && model.linksForActionButtons.length > 0
|
||||
|
||||
Layout.preferredHeight: Style.trayWindowHeaderHeight * 0.85
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
|
||||
flickable: root.flickable
|
||||
|
||||
onTriggerAction: activityModel.slotTriggerAction(model.index, actionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
103
src/gui/tray/ActivityItemActions.qml
Normal file
103
src/gui/tray/ActivityItemActions.qml
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
src/gui/tray/ActivityItemContent.qml
Normal file
127
src/gui/tray/ActivityItemContent.qml
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
25
src/gui/tray/ActivityItemContextMenu.qml
Normal file
25
src/gui/tray/ActivityItemContextMenu.qml
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
61
src/gui/tray/CustomButton.qml
Normal file
61
src/gui/tray/CustomButton.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
49
src/gui/tray/CustomTextButton.qml
Normal file
49
src/gui/tray/CustomTextButton.qml
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ Window {
|
|||
height: 500
|
||||
|
||||
ActivityList {
|
||||
isFileActivityList: true
|
||||
anchors.fill: parent
|
||||
model: dialog.model
|
||||
}
|
||||
|
|
|
@ -748,7 +748,7 @@ Window {
|
|||
openFileActivityDialog(displayPath, absolutePath)
|
||||
}
|
||||
onActivityItemClicked: {
|
||||
model.triggerDefaultAction(index)
|
||||
model.slotTriggerDefaultAction(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
const auto itFound = std::find_if(
|
||||
std::cbegin(_activityData), std::cend(_activityData), [&sinceId](const QJsonValue ¤tActivityValue) {
|
||||
const auto currentActivityId =
|
||||
currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt();
|
||||
return currentActivityId == sinceId;
|
||||
});
|
||||
|
||||
if(_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt() > sinceId) {
|
||||
data.append(_activityData[dataIndex]);
|
||||
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 ¤tActivityValue) {
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue