Merge pull request #4189 from nextcloud/feature/activity_thumbnails

Add thumbnails for files in the activity view
This commit is contained in:
Claudio Cambra 2022-03-17 11:57:06 +01:00 committed by GitHub
commit 6aefe8f2e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 835 additions and 261 deletions

View file

@ -195,10 +195,10 @@ set(client_SRCS
tray/activitylistmodel.h tray/activitylistmodel.h
tray/activitylistmodel.cpp tray/activitylistmodel.cpp
tray/unifiedsearchresult.h tray/unifiedsearchresult.h
tray/asyncimageresponse.cpp
tray/unifiedsearchresult.cpp tray/unifiedsearchresult.cpp
tray/unifiedsearchresultimageprovider.h
tray/unifiedsearchresultimageprovider.cpp
tray/unifiedsearchresultslistmodel.h tray/unifiedsearchresultslistmodel.h
tray/trayimageprovider.cpp
tray/unifiedsearchresultslistmodel.cpp tray/unifiedsearchresultslistmodel.cpp
tray/usermodel.h tray/usermodel.h
tray/usermodel.cpp tray/usermodel.cpp

View file

@ -20,7 +20,7 @@
#include "tray/svgimageprovider.h" #include "tray/svgimageprovider.h"
#include "tray/usermodel.h" #include "tray/usermodel.h"
#include "wheelhandler.h" #include "wheelhandler.h"
#include "tray/unifiedsearchresultimageprovider.h" #include "tray/trayimageprovider.h"
#include "configfile.h" #include "configfile.h"
#include "accessmanager.h" #include "accessmanager.h"
@ -65,7 +65,7 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
_trayEngine->addImportPath("qrc:/qml/theme"); _trayEngine->addImportPath("qrc:/qml/theme");
_trayEngine->addImageProvider("avatars", new ImageProvider); _trayEngine->addImageProvider("avatars", new ImageProvider);
_trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider); _trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider);
_trayEngine->addImageProvider(QLatin1String("unified-search-result-icon"), new UnifiedSearchResultImageProvider); _trayEngine->addImageProvider(QLatin1String("tray-image-provider"), new TrayImageProvider);
} }
Systray::Systray() Systray::Systray()
@ -513,7 +513,11 @@ AccessManagerFactory::AccessManagerFactory()
QNetworkAccessManager* AccessManagerFactory::create(QObject *parent) QNetworkAccessManager* AccessManagerFactory::create(QObject *parent)
{ {
return new AccessManager(parent); const auto am = new AccessManager(parent);
const auto diskCache = new QNetworkDiskCache(am);
diskCache->setCacheDirectory("cacheDir");
am->setCache(diskCache);
return am;
} }
} // namespace OCC } // namespace OCC

View file

@ -38,8 +38,8 @@ MouseArea {
ColumnLayout { ColumnLayout {
anchors.left: root.left anchors.left: root.left
anchors.right: root.right anchors.right: root.right
anchors.leftMargin: 15
anchors.rightMargin: 10 anchors.rightMargin: 10
anchors.leftMargin: 10
spacing: 0 spacing: 0

View file

@ -3,6 +3,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import Style 1.0 import Style 1.0
import QtGraphicalEffects 1.15
import com.nextcloud.desktopclient 1.0 import com.nextcloud.desktopclient 1.0
RowLayout { RowLayout {
@ -19,19 +20,66 @@ RowLayout {
signal dismissButtonClicked() signal dismissButtonClicked()
signal shareButtonClicked() signal shareButtonClicked()
spacing: 10 spacing: Style.trayHorizontalMargin
Image {
id: activityIcon
Item {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.preferredWidth: 32 Layout.preferredWidth: Style.trayListItemIconSize
Layout.preferredHeight: 32 Layout.preferredHeight: Style.trayListItemIconSize
verticalAlignment: Qt.AlignCenter Loader {
source: icon id: thumbnailImageLoader
sourceSize.height: 64 anchors.fill: parent
sourceSize.width: 64 active: model.thumbnail !== undefined
sourceComponent: Item {
anchors.fill: parent
Image {
id: thumbnailImage
width: model.thumbnail.isMimeTypeIcon ? parent.width * 0.85 : parent.width * 0.8
height: model.thumbnail.isMimeTypeIcon ? parent.height * 0.85 : parent.height * 0.8
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
cache: true
source: model.thumbnail.source
visible: false
sourceSize.height: 64
sourceSize.width: 64
}
Rectangle {
id: mask
color: "white"
radius: 3
anchors.fill: thumbnailImage
visible: false
width: thumbnailImage.paintedWidth
height: thumbnailImage.paintedHeight
}
OpacityMask {
anchors.fill: thumbnailImage
source: thumbnailImage
maskSource: mask
visible: model.thumbnail !== undefined
}
}
}
Image {
id: activityIcon
width: model.thumbnail !== undefined ? parent.width * 0.5 : parent.width * 0.85
height: model.thumbnail !== undefined ? parent.height * 0.5 : parent.height * 0.85
anchors.verticalCenter: if(model.thumbnail === undefined) parent.verticalCenter
anchors.left: if(model.thumbnail === undefined) parent.left
anchors.right: if(model.thumbnail !== undefined) parent.right
anchors.bottom: if(model.thumbnail !== undefined) parent.bottom
cache: true
source: icon
sourceSize.height: 64
sourceSize.width: 64
}
} }
Column { Column {

View file

@ -11,7 +11,7 @@ RowLayout {
property alias model: syncStatus property alias model: syncStatus
spacing: 0 spacing: Style.trayHorizontalMargin
NC.SyncStatusSummary { NC.SyncStatusSummary {
id: syncStatus id: syncStatus
@ -19,15 +19,18 @@ RowLayout {
Image { Image {
id: syncIcon id: syncIcon
Layout.preferredWidth: Style.trayListItemIconSize * 0.85
Layout.preferredHeight: Style.trayListItemIconSize * 0.85
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.topMargin: 16 Layout.topMargin: 16
Layout.rightMargin: Style.trayListItemIconSize * 0.15
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: Style.trayHorizontalMargin
source: syncStatus.syncIcon source: syncStatus.syncIcon
sourceSize.width: 32 sourceSize.width: 64
sourceSize.height: 32 sourceSize.height: 64
rotation: syncStatus.syncing ? 0 : 0 rotation: syncStatus.syncing ? 0 : 0
} }
@ -45,8 +48,7 @@ RowLayout {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.topMargin: 8 Layout.topMargin: 8
Layout.rightMargin: 16 Layout.rightMargin: Style.trayHorizontalMargin
Layout.leftMargin: 10
Layout.bottomMargin: 8 Layout.bottomMargin: 8
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -65,7 +67,7 @@ RowLayout {
Loader { Loader {
Layout.fillWidth: true Layout.fillWidth: true
active: syncStatus.syncing; active: syncStatus.syncing
visible: syncStatus.syncing visible: syncStatus.syncing
sourceComponent: ProgressBar { sourceComponent: ProgressBar {

View file

@ -13,15 +13,15 @@ TextField {
readonly property color textFieldIconsColor: Style.menuBorder readonly property color textFieldIconsColor: Style.menuBorder
readonly property int textFieldIconsOffset: 10 readonly property int textFieldIconsOffset: Style.trayHorizontalMargin
readonly property double textFieldIconsScaleFactor: 0.6 readonly property double textFieldIconsScaleFactor: 0.6
readonly property int textFieldHorizontalPaddingOffset: 14 readonly property int textFieldHorizontalPaddingOffset: Style.trayHorizontalMargin
signal clearText() signal clearText()
leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset - 1
rightPadding: trayWindowUnifiedSearchTextFieldClearTextButton.width + trayWindowUnifiedSearchTextFieldClearTextButton.anchors.rightMargin + textFieldHorizontalPaddingOffset rightPadding: trayWindowUnifiedSearchTextFieldClearTextButton.width + trayWindowUnifiedSearchTextFieldClearTextButton.anchors.rightMargin + textFieldHorizontalPaddingOffset
placeholderText: qsTr("Search files, messages, events …") placeholderText: qsTr("Search files, messages, events …")
@ -36,6 +36,9 @@ TextField {
Image { Image {
id: trayWindowUnifiedSearchTextFieldSearchIcon id: trayWindowUnifiedSearchTextFieldSearchIcon
width: Style.trayListItemIconSize - anchors.leftMargin
fillMode: Image.PreserveAspectFit
horizontalAlignment: Image.AlignLeft
anchors { anchors {
left: parent.left left: parent.left

View file

@ -39,7 +39,7 @@ RowLayout {
id: unifiedSearchResultThumbnail id: unifiedSearchResultThumbnail
visible: false visible: false
asynchronous: true asynchronous: true
source: "image://unified-search-result-icon/" + icons source: "image://tray-image-provider/" + icons
cache: true cache: true
sourceSize.width: imageData.width sourceSize.width: imageData.width
sourceSize.height: imageData.height sourceSize.height: imageData.height

View file

@ -22,8 +22,8 @@ Window {
color: "transparent" color: "transparent"
flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint
property int fileActivityDialogObjectId: -1 property int fileActivityDialogObjectId: -1
readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth
function openFileActivityDialog(objectName, objectId) { function openFileActivityDialog(objectName, objectId) {
@ -345,7 +345,7 @@ Window {
Image { Image {
id: currentAccountAvatar id: currentAccountAvatar
Layout.leftMargin: 8 Layout.leftMargin: Style.trayHorizontalMargin
verticalAlignment: Qt.AlignCenter verticalAlignment: Qt.AlignCenter
cache: false cache: false
source: UserModel.currentUser.avatar != "" ? UserModel.currentUser.avatar : "image://avatars/fallbackWhite" source: UserModel.currentUser.avatar != "" ? UserModel.currentUser.avatar : "image://avatars/fallbackWhite"
@ -601,9 +601,9 @@ Window {
left: trayWindowBackground.left left: trayWindowBackground.left
right: trayWindowBackground.right right: trayWindowBackground.right
margins: { topMargin: Style.trayHorizontalMargin + controlRoot.padding
top: 10 leftMargin: Style.trayHorizontalMargin + controlRoot.padding
} rightMargin: Style.trayHorizontalMargin + controlRoot.padding
} }
text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm
@ -623,7 +623,7 @@ Window {
anchors.top: trayWindowUnifiedSearchInputContainer.bottom anchors.top: trayWindowUnifiedSearchInputContainer.bottom
anchors.left: trayWindowBackground.left anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right anchors.right: trayWindowBackground.right
anchors.margins: 10 anchors.margins: Style.trayHorizontalMargin
} }
UnifiedSearchResultNothingFound { UnifiedSearchResultNothingFound {
@ -632,7 +632,7 @@ Window {
anchors.top: trayWindowUnifiedSearchInputContainer.bottom anchors.top: trayWindowUnifiedSearchInputContainer.bottom
anchors.left: trayWindowBackground.left anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right anchors.right: trayWindowBackground.right
anchors.topMargin: 10 anchors.topMargin: Style.trayHorizontalMargin
text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm

View file

@ -15,6 +15,7 @@
#include <QtCore> #include <QtCore>
#include "activitydata.h" #include "activitydata.h"
#include "folderman.h"
namespace OCC { namespace OCC {
@ -44,4 +45,99 @@ ActivityLink ActivityLink::createFomJsonObject(const QJsonObject &obj)
return activityLink; return activityLink;
} }
OCC::Activity Activity::fromActivityJson(const QJsonObject json, const AccountPtr account)
{
const auto activityUser = json.value(QStringLiteral("user")).toString();
Activity activity;
activity._type = Activity::ActivityType;
activity._objectType = json.value(QStringLiteral("object_type")).toString();
activity._objectId = json.value(QStringLiteral("object_id")).toInt();
activity._objectName = json.value(QStringLiteral("object_name")).toString();
activity._id = json.value(QStringLiteral("activity_id")).toInt();
activity._fileAction = json.value(QStringLiteral("type")).toString();
activity._accName = account->displayName();
activity._subject = json.value(QStringLiteral("subject")).toString();
activity._message = json.value(QStringLiteral("message")).toString();
activity._file = json.value(QStringLiteral("object_name")).toString();
activity._link = QUrl(json.value(QStringLiteral("link")).toString());
activity._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
activity._icon = json.value(QStringLiteral("icon")).toString();
activity._isCurrentUserFileActivity = activity._objectType == QStringLiteral("files") && activityUser == account->davUser();
auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
if(richSubjectData.size() > 1) {
activity._subjectRich = richSubjectData[0].toString();
auto parameters = richSubjectData[1].toObject();
const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
for (auto i = parameters.begin(); i != parameters.end(); ++i) {
const auto parameterJsonObject = i.value().toObject();
activity._subjectRichParameters[i.key()] = Activity::RichSubjectParameter {
parameterJsonObject.value(QStringLiteral("type")).toString(),
parameterJsonObject.value(QStringLiteral("id")).toString(),
parameterJsonObject.value(QStringLiteral("name")).toString(),
parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
};
}
auto displayString = activity._subjectRich;
auto subjectRichParameterMatch = subjectRichParameterRe.globalMatch(displayString);
while (subjectRichParameterMatch.hasNext()) {
const auto match = subjectRichParameterMatch.next();
auto word = match.captured(1);
word.remove(subjectRichParameterBracesRe);
Q_ASSERT(activity._subjectRichParameters.contains(word));
displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].name);
}
activity._subjectDisplay = displayString;
}
const auto previewsData = json.value(QStringLiteral("previews")).toArray();
for(const auto preview : previewsData) {
const auto jsonPreviewData = preview.toObject();
PreviewData data;
data._link = jsonPreviewData.value(QStringLiteral("link")).toString();
data._mimeType = jsonPreviewData.value(QStringLiteral("mimeType")).toString();
data._fileId = jsonPreviewData.value(QStringLiteral("fileId")).toInt();
data._view = jsonPreviewData.value(QStringLiteral("view")).toString();
data._filename = jsonPreviewData.value(QStringLiteral("filename")).toString();
if(data._mimeType.contains(QStringLiteral("text/"))) {
data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg");
data._isMimeTypeIcon = true;
} else if (data._mimeType.contains(QStringLiteral("/pdf"))) {
data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg");
data._isMimeTypeIcon = true;
} else {
data._source = jsonPreviewData.value(QStringLiteral("source")).toString();
data._isMimeTypeIcon = jsonPreviewData.value(QStringLiteral("isMimeTypeIcon")).toBool();
}
activity._previews.append(data);
}
if(!previewsData.isEmpty()) {
if(activity._icon.contains(QStringLiteral("add-color.svg"))) {
activity._icon = "qrc:///client/theme/colored/add-bordered.svg";
} else if(activity._icon.contains(QStringLiteral("delete-color.svg"))) {
activity._icon = "qrc:///client/theme/colored/delete-bordered.svg";
} else if(activity._icon.contains(QStringLiteral("change.svg"))) {
activity._icon = "qrc:///client/theme/colored/change-bordered.svg";
}
}
return activity;
}
} }

View file

@ -19,6 +19,10 @@
#include <QIcon> #include <QIcon>
#include <QJsonObject> #include <QJsonObject>
#include "syncfileitem.h"
#include "folder.h"
#include "account.h"
namespace OCC { namespace OCC {
/** /**
* @brief The ActivityLink class describes actions of an activity * @brief The ActivityLink class describes actions of an activity
@ -49,6 +53,32 @@ public:
bool _primary; bool _primary;
}; };
/**
* @brief The PreviewData class describes the data about a file's preview.
*/
class PreviewData
{
Q_GADGET
Q_PROPERTY(QString source MEMBER _source)
Q_PROPERTY(QString link MEMBER _link)
Q_PROPERTY(QString mimeType MEMBER _mimeType)
Q_PROPERTY(int fileId MEMBER _fileId)
Q_PROPERTY(QString view MEMBER _view)
Q_PROPERTY(bool isMimeTypeIcon MEMBER _isMimeTypeIcon)
Q_PROPERTY(QString filename MEMBER _filename)
public:
QString _source;
QString _link;
QString _mimeType;
int _fileId;
QString _view;
bool _isMimeTypeIcon;
QString _filename;
};
/* ==================================================================== */ /* ==================================================================== */
/** /**
* @brief Activity Structure * @brief Activity Structure
@ -69,6 +99,8 @@ public:
SyncFileItemType SyncFileItemType
}; };
static Activity fromActivityJson(const QJsonObject json, const AccountPtr account);
struct RichSubjectParameter { struct RichSubjectParameter {
QString type; // Required QString type; // Required
QString id; // Required QString id; // Required
@ -97,6 +129,7 @@ public:
QString _accName; QString _accName;
QString _icon; QString _icon;
bool _isCurrentUserFileActivity = false; bool _isCurrentUserFileActivity = false;
QVector<PreviewData> _previews;
// Stores information about the error // Stores information about the error
int _status; int _status;
@ -127,5 +160,6 @@ using ActivityList = QList<Activity>;
Q_DECLARE_METATYPE(OCC::Activity::Type) Q_DECLARE_METATYPE(OCC::Activity::Type)
Q_DECLARE_METATYPE(OCC::ActivityLink) Q_DECLARE_METATYPE(OCC::ActivityLink)
Q_DECLARE_METATYPE(OCC::PreviewData)
#endif // ACTIVITYDATA_H #endif // ACTIVITYDATA_H

View file

@ -74,6 +74,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
roles[DisplayActions] = "displayActions"; roles[DisplayActions] = "displayActions";
roles[ShareableRole] = "isShareable"; roles[ShareableRole] = "isShareable";
roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity"; roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
roles[ThumbnailRole] = "thumbnail";
return roles; return roles;
} }
@ -175,6 +176,18 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return displayPath == "." || displayPath == "/" ? QString() : displayPath; return displayPath == "." || displayPath == "/" ? QString() : displayPath;
}; };
const auto generatePreviewMap = [](const PreviewData &preview) {
return(QVariantMap {
{QStringLiteral("source"), QStringLiteral("image://tray-image-provider/").append(preview._source)},
{QStringLiteral("link"), preview._link},
{QStringLiteral("mimeType"), preview._mimeType},
{QStringLiteral("fileId"), preview._fileId},
{QStringLiteral("view"), preview._view},
{QStringLiteral("isMimeTypeIcon"), preview._isMimeTypeIcon},
{QStringLiteral("filename"), preview._filename},
});
};
switch (role) { switch (role) {
case DisplayPathRole: case DisplayPathRole:
return getDisplayPath(); return getDisplayPath();
@ -220,11 +233,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
} else { } else {
// File sync successful // File sync successful
if (a._fileAction == "file_created") { if (a._fileAction == "file_created") {
return "qrc:///client/theme/colored/add.svg"; return a._previews.empty() ? "qrc:///client/theme/colored/add.svg"
: "qrc:///client/theme/colored/add-bordered.svg";
} else if (a._fileAction == "file_deleted") { } else if (a._fileAction == "file_deleted") {
return "qrc:///client/theme/colored/delete.svg"; return a._previews.empty() ? "qrc:///client/theme/colored/delete.svg"
: "qrc:///client/theme/colored/delete-bordered.svg";
} else { } else {
return "qrc:///client/theme/change.svg"; return a._previews.empty() ? "qrc:///client/theme/change.svg"
: "qrc:///client/theme/colored/change-bordered.svg";
} }
} }
} else { } else {
@ -286,6 +302,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _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: case IsCurrentUserFileActivityRole:
return a._isCurrentUserFileActivity; return a._isCurrentUserFileActivity;
case ThumbnailRole: {
if(a._previews.empty()) {
return {};
}
const auto preview = a._previews[0];
return(generatePreviewMap(preview));
}
default: default:
return QVariant(); return QVariant();
} }
@ -324,6 +348,7 @@ void ActivityListModel::startFetchJob()
this, &ActivityListModel::activitiesReceived); this, &ActivityListModel::activitiesReceived);
QUrlQuery params; QUrlQuery params;
params.addQueryItem(QLatin1String("previews"), QLatin1String("true"));
params.addQueryItem(QLatin1String("since"), QString::number(_currentItem)); params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
params.addQueryItem(QLatin1String("limit"), QString::number(50)); params.addQueryItem(QLatin1String("limit"), QString::number(50));
job->addQueryParams(params); job->addQueryParams(params);
@ -348,80 +373,17 @@ int ActivityListModel::currentItem() const
return _currentItem; return _currentItem;
} }
void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode) void ActivityListModel::ingestActivities(const QJsonArray &activities)
{ {
auto activities = json.object().value("ocs").toObject().value("data").toArray();
ActivityList list; ActivityList list;
auto ast = _accountState;
if (!ast) {
return;
}
if (activities.size() == 0) {
_doneFetching = true;
}
_currentlyFetching = false;
QDateTime oldestDate = QDateTime::currentDateTime(); QDateTime oldestDate = QDateTime::currentDateTime();
oldestDate = oldestDate.addDays(_maxActivitiesDays * -1); oldestDate = oldestDate.addDays(_maxActivitiesDays * -1);
foreach (auto activ, activities) { for (const auto &activ : activities) {
auto json = activ.toObject(); const auto json = activ.toObject();
Activity a; const auto a = Activity::fromActivityJson(json, _accountState->account());
const auto activityUser = json.value(QStringLiteral("user")).toString();
a._type = Activity::ActivityType;
a._objectType = json.value(QStringLiteral("object_type")).toString();
a._objectId = json.value(QStringLiteral("object_id")).toInt();
a._objectName = json.value(QStringLiteral("object_name")).toString();
a._accName = ast->account()->displayName();
a._id = json.value(QStringLiteral("activity_id")).toInt();
a._fileAction = json.value(QStringLiteral("type")).toString();
a._subject = json.value(QStringLiteral("subject")).toString();
a._message = json.value(QStringLiteral("message")).toString();
a._file = json.value(QStringLiteral("object_name")).toString();
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();
if(richSubjectData.size() > 1) {
a._subjectRich = richSubjectData[0].toString();
auto parameters = richSubjectData[1].toObject();
const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
for (auto i = parameters.begin(); i != parameters.end(); ++i) {
const auto parameterJsonObject = i.value().toObject();
const Activity::RichSubjectParameter parameter = {
parameterJsonObject.value(QStringLiteral("type")).toString(),
parameterJsonObject.value(QStringLiteral("id")).toString(),
parameterJsonObject.value(QStringLiteral("name")).toString(),
parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
};
a._subjectRichParameters[i.key()] = parameter;
}
auto displayString = a._subjectRich;
auto i = subjectRichParameterRe.globalMatch(displayString);
while (i.hasNext()) {
const auto match = i.next();
auto word = match.captured(1);
word.remove(subjectRichParameterBracesRe);
Q_ASSERT(a._subjectRichParameters.contains(word));
displayString = displayString.replace(match.captured(1), a._subjectRichParameters[word].name);
}
a._subjectDisplay = displayString;
}
list.append(a); list.append(a);
_currentItem = list.last()._id; _currentItem = list.last()._id;
@ -436,6 +398,23 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
} }
_activityLists.append(list); _activityLists.append(list);
}
void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
{
const auto activities = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray();
if (!_accountState) {
return;
}
if (activities.empty()) {
_doneFetching = true;
}
_currentlyFetching = false;
ingestActivities(activities);
combineActivityLists(); combineActivityLists();

View file

@ -66,6 +66,7 @@ public:
DisplayActions, DisplayActions,
ShareableRole, ShareableRole,
IsCurrentUserFileActivityRole, IsCurrentUserFileActivityRole,
ThumbnailRole,
}; };
Q_ENUM(DataRole) Q_ENUM(DataRole)
@ -136,6 +137,8 @@ private:
void combineActivityLists(); void combineActivityLists();
bool canFetchActivities() const; bool canFetchActivities() const;
void ingestActivities(const QJsonArray &activities);
ActivityList _activityLists; ActivityList _activityLists;
ActivityList _syncFileItemLists; ActivityList _syncFileItemLists;
ActivityList _notificationLists; ActivityList _notificationLists;

View file

@ -0,0 +1,109 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QIcon>
#include <QPainter>
#include <QSvgRenderer>
#include "asyncimageresponse.h"
#include "usermodel.h"
AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize)
{
if (id.isEmpty()) {
setImageAndEmitFinished();
return;
}
_imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts);
_requestedImageSize = requestedSize;
if (_imagePaths.isEmpty()) {
setImageAndEmitFinished();
} else {
processNextImage();
}
}
void AsyncImageResponse::setImageAndEmitFinished(const QImage &image)
{
_image = image;
emit finished();
}
QQuickTextureFactory* AsyncImageResponse::textureFactory() const
{
return QQuickTextureFactory::textureFactoryForImage(_image);
}
void AsyncImageResponse::processNextImage()
{
if (_index < 0 || _index >= _imagePaths.size()) {
setImageAndEmitFinished();
return;
}
if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
return;
}
const auto currentUser = OCC::UserModel::instance()->currentUser();
if (currentUser && currentUser->account()) {
const QUrl iconUrl(_imagePaths.at(_index));
if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) {
// fetch the remote resource
const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
++_index;
return;
}
}
setImageAndEmitFinished();
}
void AsyncImageResponse::slotProcessNetworkReply()
{
const auto reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) {
setImageAndEmitFinished();
return;
}
const QByteArray imageData = reply->readAll();
// server returns "[]" for some some file previews (have no idea why), so, we use another image
// from the list if available
if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) {
processNextImage();
} else {
if (imageData.startsWith(QByteArrayLiteral("<svg"))) {
// SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer
QSvgRenderer svgRenderer;
if (svgRenderer.load(imageData)) {
QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32);
scaledSvg.fill("transparent");
QPainter painterForSvg(&scaledSvg);
svgRenderer.render(&painterForSvg);
setImageAndEmitFinished(scaledSvg);
return;
} else {
processNextImage();
}
} else {
setImageAndEmitFinished(QImage::fromData(imageData));
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QImage>
#include <QQuickImageProvider>
class AsyncImageResponse : public QQuickImageResponse
{
public:
AsyncImageResponse(const QString &id, const QSize &requestedSize);
void setImageAndEmitFinished(const QImage &image = {});
QQuickTextureFactory *textureFactory() const override;
private:
void processNextImage();
private slots:
void slotProcessNetworkReply();
QImage _image;
QStringList _imagePaths;
QSize _requestedImageSize;
int _index = 0;
};

View file

@ -0,0 +1,25 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "trayimageprovider.h"
#include "asyncimageresponse.h"
namespace OCC {
QQuickImageResponse *TrayImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new AsyncImageResponse(id, requestedSize);
}
}

View file

@ -20,12 +20,12 @@
namespace OCC { namespace OCC {
/** /**
* @brief The UnifiedSearchResultImageProvider * @brief The TrayImageProvider
* @ingroup gui * @ingroup gui
* Allows to fetch Unified Search result icon from the server or used a local resource * Allows to fetch icon from the server or used a local resource
*/ */
class UnifiedSearchResultImageProvider : public QQuickAsyncImageProvider class TrayImageProvider : public QQuickAsyncImageProvider
{ {
public: public:
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;

View file

@ -1,131 +0,0 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "unifiedsearchresultimageprovider.h"
#include "usermodel.h"
#include <QImage>
#include <QPainter>
#include <QSvgRenderer>
namespace {
class AsyncImageResponse : public QQuickImageResponse
{
public:
AsyncImageResponse(const QString &id, const QSize &requestedSize)
{
if (id.isEmpty()) {
setImageAndEmitFinished();
return;
}
_imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts);
_requestedImageSize = requestedSize;
if (_imagePaths.isEmpty()) {
setImageAndEmitFinished();
} else {
processNextImage();
}
}
void setImageAndEmitFinished(const QImage &image = {})
{
_image = image;
emit finished();
}
QQuickTextureFactory *textureFactory() const override
{
return QQuickTextureFactory::textureFactoryForImage(_image);
}
private:
void processNextImage()
{
if (_index < 0 || _index >= _imagePaths.size()) {
setImageAndEmitFinished();
return;
}
if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) {
setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage());
return;
}
const auto currentUser = OCC::UserModel::instance()->currentUser();
if (currentUser && currentUser->account()) {
const QUrl iconUrl(_imagePaths.at(_index));
if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) {
// fetch the remote resource
const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl);
connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply);
++_index;
return;
}
}
setImageAndEmitFinished();
}
private slots:
void slotProcessNetworkReply()
{
const auto reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) {
setImageAndEmitFinished();
return;
}
const QByteArray imageData = reply->readAll();
// server returns "[]" for some some file previews (have no idea why), so, we use another image
// from the list if available
if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) {
processNextImage();
} else {
if (imageData.startsWith(QByteArrayLiteral("<svg"))) {
// SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer
QSvgRenderer svgRenderer;
if (svgRenderer.load(imageData)) {
QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32);
scaledSvg.fill("transparent");
QPainter painterForSvg(&scaledSvg);
svgRenderer.render(&painterForSvg);
setImageAndEmitFinished(scaledSvg);
return;
} else {
processNextImage();
}
} else {
setImageAndEmitFinished(QImage::fromData(imageData));
}
}
}
QImage _image;
QStringList _imagePaths;
QSize _requestedImageSize;
int _index = 0;
};
}
namespace OCC {
QQuickImageResponse *UnifiedSearchResultImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new AsyncImageResponse(id, requestedSize);
}
}

View file

@ -16,6 +16,7 @@
#include "tray/notificationcache.h" #include "tray/notificationcache.h"
#include "tray/unifiedsearchresultslistmodel.h" #include "tray/unifiedsearchresultslistmodel.h"
#include "userstatusconnector.h" #include "userstatusconnector.h"
#include "thumbnailjob.h"
#include <QDesktopServices> #include <QDesktopServices>
#include <QIcon> #include <QIcon>
@ -499,50 +500,88 @@ bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const
void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item) void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item)
{ {
const auto fileActionFromInstruction = [](const int instruction) {
if (instruction == CSYNC_INSTRUCTION_REMOVE) {
return QStringLiteral("file_deleted");
} else if (instruction == CSYNC_INSTRUCTION_NEW) {
return QStringLiteral("file_created");
} else if (instruction == CSYNC_INSTRUCTION_RENAME) {
return QStringLiteral("file_renamed");
} else {
return QStringLiteral("file_changed");
}
};
const auto messageFromFileAction = [](const QString &fileAction, const QString &fileName) {
if (fileAction == QStringLiteral("file_renamed")) {
return QObject::tr("You renamed %1").arg(fileName);
} else if (fileAction == QStringLiteral("file_deleted")) {
return QObject:: tr("You deleted %1").arg(fileName);
} else if (fileAction == QStringLiteral("file_created")) {
return QObject::tr("You created %1").arg(fileName);
} else {
return QObject::tr("You changed %1").arg(fileName);
}
};
Activity activity; Activity activity;
activity._type = Activity::SyncFileItemType; //client activity activity._type = Activity::SyncFileItemType; //client activity
activity._status = item->_status; activity._status = item->_status;
activity._dateTime = QDateTime::currentDateTime(); activity._dateTime = QDateTime::currentDateTime();
activity._message = item->_originalFile; activity._message = item->_originalFile;
activity._link = folder->accountState()->account()->url(); activity._link = account()->url();
activity._accName = folder->accountState()->account()->displayName(); activity._accName = account()->displayName();
activity._file = item->_file; activity._file = item->_file;
activity._folder = folder->alias(); activity._folder = folder->alias();
activity._fileAction = ""; activity._fileAction = "";
activity._objectId = item->_fileId.toInt();
activity._objectName = item->_file;
const auto fileName = QFileInfo(item->_originalFile).fileName(); const auto fileName = QFileInfo(item->_originalFile).fileName();
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) { activity._fileAction = fileActionFromInstruction(item->_instruction);
activity._fileAction = "file_deleted";
} else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
activity._fileAction = "file_created";
} else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
activity._fileAction = "file_renamed";
activity._renamedFile = item->_renameTarget;
} else {
activity._fileAction = "file_changed";
}
if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) { if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully."; qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
if (item->_direction != SyncFileItem::Up) { if (item->_direction != SyncFileItem::Up) {
activity._message = tr("Synced %1").arg(fileName); activity._message = QObject::tr("Synced %1").arg(fileName);
} else if (activity._fileAction == "file_renamed") {
activity._message = tr("You renamed %1").arg(fileName);
} else if (activity._fileAction == "file_deleted") {
activity._message = tr("You deleted %1").arg(fileName);
} else if (activity._fileAction == "file_created") {
activity._message = tr("You created %1").arg(fileName);
} else { } else {
activity._message = tr("You changed %1").arg(fileName); activity._message = messageFromFileAction(activity._fileAction, fileName);
}
if(activity._fileAction != "file_deleted") {
auto remotePath = folder->remotePath();
remotePath.append(activity._fileAction == "file_renamed" ? item->_renameTarget : activity._file);
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(item->_file, account());
if (!localFiles.isEmpty()) {
const QMimeType mimeType = _mimeDb.mimeTypeForFile(QFileInfo(localFiles.constFirst()));
// Set the preview data, though for now we can skip setting file ID, link, and view
PreviewData preview;
preview._mimeType = mimeType.name();
preview._filename = fileName;
if(item->isDirectory()) {
preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/folder.svg");
preview._isMimeTypeIcon = true;
} else if(mimeType.isValid() && mimeType.inherits("text/plain")) {
preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg");
preview._isMimeTypeIcon = true;
} else if (mimeType.isValid() && mimeType.inherits("application/pdf")) {
preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg");
preview._isMimeTypeIcon = true;
} else {
preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/files/api/v1/thumbnail/150/150/") + remotePath;
preview._isMimeTypeIcon = false;
}
activity._previews.append(preview);
}
} }
_activityModel->addSyncFileItemToActivityList(activity); _activityModel->addSyncFileItemToActivityList(activity);
} else { } else {
qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString; qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
activity._subject = item->_errorString; activity._subject = item->_errorString;
if (item->_status == SyncFileItem::Status::FileIgnored) { if (item->_status == SyncFileItem::Status::FileIgnored) {

View file

@ -134,6 +134,7 @@ private:
QElapsedTimer _guiLogTimer; QElapsedTimer _guiLogTimer;
NotificationCache _notificationCache; NotificationCache _notificationCache;
QMimeDatabase _mimeDb;
// number of currently running notification requests. If non zero, // number of currently running notification requests. If non zero,
// no query for notifications is started. // no query for notifications is started.

View file

@ -62,6 +62,7 @@ nextcloud_add_test(NotificationCache)
nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(SetUserStatusDialog)
nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(UnifiedSearchListmodel)
nextcloud_add_test(ActivityListModel) nextcloud_add_test(ActivityListModel)
nextcloud_add_test(ActivityData)
if( UNIX AND NOT APPLE ) if( UNIX AND NOT APPLE )
nextcloud_add_test(InotifyWatcher) nextcloud_add_test(InotifyWatcher)

184
test/testactivitydata.cpp Normal file
View file

@ -0,0 +1,184 @@
/*
* Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "gui/tray/activitydata.h"
#include "account.h"
#include "accountstate.h"
#include "configfile.h"
#include "syncenginetestutils.h"
#include "syncfileitem.h"
#include "folder.h"
#include "folderman.h"
#include "testhelper.h"
#include <QTest>
class TestActivityData : public QObject
{
Q_OBJECT
public:
TestActivityData() = default;
void createJsonSpecificFormatData(QString fileFormat, QString mimeType)
{
const auto objectType = QStringLiteral("files");
const auto subject = QStringLiteral("You created path/test.").append(fileFormat);
const auto path = QStringLiteral("path/test.").append(fileFormat);
const auto fileName = QStringLiteral("test.").append(fileFormat);
const auto activityType = QStringLiteral("file");
const auto activityId = 90000;
const auto message = QStringLiteral();
const auto objectName = QStringLiteral("test.").append(fileFormat);
const auto link = account->url().toString().append(QStringLiteral("/f/")).append(activityId);
const auto datetime = QDateTime::currentDateTime().toString(Qt::ISODate);
const auto icon = account->url().toString().append(QStringLiteral("/apps/files/img/add-color.svg"));
const QJsonObject richStringData({
{QStringLiteral("type"), activityType},
{QStringLiteral("id"), activityId},
{QStringLiteral("link"), link},
{QStringLiteral("name"), fileName},
{QStringLiteral("path"), objectName}
});
const auto subjectRichString = QStringLiteral("You created {file1}");
const auto subjectRichObj = QJsonObject({{QStringLiteral("file1"), richStringData}});
const auto subjectRichData = QJsonArray({subjectRichString, subjectRichObj});
const auto previewUrl = account->url().toString().append(QStringLiteral("/index.php/core/preview.png?file=/")).append(path);
// Text file previews should be replaced by mimetype icon
const QJsonObject previewData({
{QStringLiteral("link"), link},
{QStringLiteral("mimeType"), mimeType},
{QStringLiteral("fileId"), activityId},
{QStringLiteral("filename"), fileName},
{QStringLiteral("view"), QStringLiteral("files")},
{QStringLiteral("source"), previewUrl},
{QStringLiteral("isMimeTypeIcon"), false},
});
QJsonObject testData({
{QStringLiteral("object_type"), objectType},
{QStringLiteral("activity_id"), activityId},
{QStringLiteral("type"), activityType},
{QStringLiteral("subject"), subject},
{QStringLiteral("message"), message},
{QStringLiteral("object_name"), objectName},
{QStringLiteral("link"), link},
{QStringLiteral("datetime"), datetime},
{QStringLiteral("icon"), icon},
{QStringLiteral("subject_rich"), subjectRichData},
{QStringLiteral("previews"), QJsonArray({previewData})},
});
QTest::addRow("data") << testData << fileFormat << mimeType << objectType << subject << path << fileName << activityType << activityId << message << objectName << link << datetime << icon << subjectRichString << subjectRichData << previewUrl;
}
QScopedPointer<FakeQNAM> fakeQnam;
OCC::AccountPtr account;
private slots:
void initTestCase()
{
account = OCC::Account::create();
account->setCredentials(new FakeCredentials{fakeQnam.data()});
account->setUrl(QUrl(("http://example.de")));
auto *cred = new HttpCredentialsTest("testuser", "secret");
account->setCredentials(cred);
}
void testFromJson_data()
{
QTest::addColumn<QJsonObject>("activityJsonObject");
QTest::addColumn<QString>("fileFormat");
QTest::addColumn<QString>("mimeTypeExpected");
QTest::addColumn<QString>("objectTypeExpected");
QTest::addColumn<QString>("subjectExpected");
QTest::addColumn<QString>("pathExpected");
QTest::addColumn<QString>("fileNameExpected");
QTest::addColumn<QString>("activityTypeExpected");
QTest::addColumn<int>("activityIdExpected");
QTest::addColumn<QString>("messageExpected");
QTest::addColumn<QString>("objectNameExpected");
QTest::addColumn<QString>("linkExpected");
QTest::addColumn<QString>("datetimeExpected");
QTest::addColumn<QString>("iconExpected");
QTest::addColumn<QString>("subjectRichStringExpected");
QTest::addColumn<QJsonArray>("subjectRichDataExpected");
QTest::addColumn<QString>("previewUrlExpected");
createJsonSpecificFormatData(QStringLiteral("jpg"), QStringLiteral("image/jpg"));
createJsonSpecificFormatData(QStringLiteral("txt"), QStringLiteral("text/plain"));
createJsonSpecificFormatData(QStringLiteral("pdf"), QStringLiteral("application/pdf"));
}
void testFromJson()
{
QFETCH(QJsonObject, activityJsonObject);
QFETCH(QString, fileFormat);
QFETCH(QString, mimeTypeExpected);
QFETCH(QString, objectTypeExpected);
QFETCH(QString, subjectExpected);
QFETCH(QString, pathExpected);
QFETCH(QString, fileNameExpected);
QFETCH(QString, activityTypeExpected);
QFETCH(int, activityIdExpected);
QFETCH(QString, messageExpected);
QFETCH(QString, objectNameExpected);
QFETCH(QString, linkExpected);
QFETCH(QString, datetimeExpected);
QFETCH(QString, iconExpected);
QFETCH(QString, subjectRichStringExpected);
QFETCH(QJsonArray, subjectRichDataExpected);
QFETCH(QString, previewUrlExpected);
OCC::Activity activity = OCC::Activity::fromActivityJson(activityJsonObject, account);
QCOMPARE(activity._type, OCC::Activity::ActivityType);
QCOMPARE(activity._objectType, objectTypeExpected);
QCOMPARE(activity._id, activityIdExpected);
QCOMPARE(activity._fileAction, activityTypeExpected);
QCOMPARE(activity._accName, account->displayName());
QCOMPARE(activity._subject, subjectExpected);
QCOMPARE(activity._message, messageExpected);
QCOMPARE(activity._file, objectNameExpected);
QCOMPARE(activity._link, linkExpected);
QCOMPARE(activity._dateTime, QDateTime::fromString(datetimeExpected, Qt::ISODate));
QCOMPARE(activity._subjectRichParameters.count(), 1);
QCOMPARE(activity._subjectDisplay, QStringLiteral("You created ").append(fileNameExpected));
QCOMPARE(activity._previews.count(), 1);
// We want the different icon when we have a preview
//QCOMPARE(activity._icon, iconExpected);
if(fileFormat == "txt") {
QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg")));
QCOMPARE(activity._previews[0]._isMimeTypeIcon, true);
QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected);
} else if(fileFormat == "pdf") {
QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg")));
QCOMPARE(activity._previews[0]._isMimeTypeIcon, true);
} else {
QCOMPARE(activity._previews[0]._source, previewUrlExpected);
QCOMPARE(activity._previews[0]._isMimeTypeIcon, false);
}
QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected);
}
};
QTEST_MAIN(TestActivityData)
#include "testactivitydata.moc"

View file

@ -190,13 +190,16 @@
<file>theme/copy.svg</file> <file>theme/copy.svg</file>
<file>theme/more.svg</file> <file>theme/more.svg</file>
<file>theme/change.svg</file> <file>theme/change.svg</file>
<file>theme/colored/change-bordered.svg</file>
<file>theme/lock-http.svg</file> <file>theme/lock-http.svg</file>
<file>theme/lock-https.svg</file> <file>theme/lock-https.svg</file>
<file>theme/lock-broken.svg</file> <file>theme/lock-broken.svg</file>
<file>theme/network.svg</file> <file>theme/network.svg</file>
<file>theme/account.svg</file> <file>theme/account.svg</file>
<file>theme/colored/add.svg</file> <file>theme/colored/add.svg</file>
<file>theme/colored/add-bordered.svg</file>
<file>theme/colored/delete.svg</file> <file>theme/colored/delete.svg</file>
<file>theme/colored/delete-bordered.svg</file>
<file>theme/colored/@APPLICATION_ICON_NAME@-icon.svg</file> <file>theme/colored/@APPLICATION_ICON_NAME@-icon.svg</file>
<file>theme/add.svg</file> <file>theme/add.svg</file>
<file>theme/share.svg</file> <file>theme/share.svg</file>

View file

@ -29,6 +29,8 @@ QtObject {
property int trayWindowRadius: 10 property int trayWindowRadius: 10
property int trayWindowBorderWidth: 1 property int trayWindowBorderWidth: 1
property int trayWindowHeaderHeight: variableSize(60) property int trayWindowHeaderHeight: variableSize(60)
property int trayHorizontalMargin: 10
property int trayListItemIconSize: accountAvatarSize
property int currentAccountButtonWidth: 220 property int currentAccountButtonWidth: 220
property int currentAccountButtonRadius: 2 property int currentAccountButtonRadius: 2
@ -58,7 +60,7 @@ QtObject {
property int roundedButtonBackgroundVerticalMargins: 5 property int roundedButtonBackgroundVerticalMargins: 5
property int userStatusEmojiSize: 8 property int userStatusEmojiSize: 8
property int userStatusSpacing: 6 property int userStatusSpacing: trayHorizontalMargin
property int userStatusAnchorsMargin: 2 property int userStatusAnchorsMargin: 2
property int accountServerAnchorsMargin: 10 property int accountServerAnchorsMargin: 10
property int accountLabelsSpacing: 4 property int accountLabelsSpacing: 4

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16"
width="16"
version="1.1"
viewbox="0 0 16 16"
id="svg4"
sodipodi:docname="add-bordered.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="39.375"
inkscape:cx="7.9873016"
inkscape:cy="8"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="1"
inkscape:window-y="1111"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
fill="#000000"
d="m 9.02,13.98 h -2 V 8.9799996 h -5 V 6.98 h 5 v -5 h 2 v 5 l 5,-0.028 v 2.0279996 h -5 z"
id="path2929"
style="fill:#000001;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
<path
fill="#000"
d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 16 16"
width="16"
version="1.1"
height="16"
id="svg6"
sodipodi:docname="change-bordered.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="27.84233"
inkscape:cx="6.0878526"
inkscape:cy="3.9867353"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="1"
inkscape:window-y="1111"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
id="path2731"
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
<path
d="M 8,2 C 5.858,2 3.875,3.145 2.804,5 L 4.752,6.125 C 5.423,4.963 6.658,4.25 7.9996,4.25 c 1.1906,0 2.297,0.56157 3,1.5 l -1.5,1.5 h 4.5 v -4.5 l -1.406,1.406 C 11.4646,2.808 9.792,2 8,2 Z"
id="path827"
style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:nodetypes="cccsccccccc" />
<path
d="m8 2c-2.142 0-4.125 1.145-5.196 3l1.948 1.125c0.671-1.162 1.906-1.875 3.2476-1.875 1.1906 0 2.297 0.56157 3 1.5l-1.5 1.5h4.5v-4.5l-1.406 1.406c-1.129-1.348-2.802-2.1563-4.594-2.1563z"
id="path2" />
<path
d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
id="path4" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16"
width="16"
version="1.1"
viewBox="0 0 16 16"
id="svg4"
sodipodi:docname="delete-bordered.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="39.375"
inkscape:cx="7.9873016"
inkscape:cy="8"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="1"
inkscape:window-y="1111"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
fill="#000"
id="path849"
style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
<path
d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
fill="#000"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB