Merge branch 'master' into clientSideEncryptionV3

This commit is contained in:
Tomaz Canabrava 2017-11-28 20:12:57 +01:00
commit 2cacf2547e
13 changed files with 484 additions and 185 deletions

119
.github/release_template.md vendored Normal file
View file

@ -0,0 +1,119 @@
<!--
This is the template for new release issues.
(originated from https://github.com/owncloud/client/wiki/Release%20Checklist%20Template)
-->
Copy below text into a task and tick the items:
```
Some weeks before the release:
* [ ] Check if we should update the bundled sqlite3 (https://github.com/owncloud/client/tree/master/src/3rdparty/sqlite3)
* [ ] Check if we should update Sparkle on build machine (https://github.com/sparkle-project/Sparkle/releases)
* [ ] Ensure NSIS is up to date on the build machine
* [ ] Ensure up-to-date dependencies (e.g. [latest Qt version](http://qt-project.org/downloads#qt-lib) is installed on the machine and picked up (cmake output)
* [ ] Ensure the crash reporter server is up
* [ ] Check crash reporter for bad crashes
* [ ] Ensure Windows Overlay DLLs are rebuilt
* [ ] Check nightly builds are up and running, that is Jenkins jobs ownCloud-client-linux, ownCloud-client-osx and ownCloud-client-win32 all green.
* [ ] Ensure Linux nightlies are built too for all distros https://build.opensuse.org/package/show/isv:ownCloud:community:nightly/owncloud-client
* [ ] Build branded clients through the scripting machine and smoke test one or two branded clients (especially with predefined url)
* [ ] Upload a nightly build of the windows version to virustotal.com
* Contact AV vendors whom's engine reports a virus
* [ ] Documentation should be online before the release http://doc.owncloud.org/desktop/1.X/
* [ ] QA goes over https://github.com/owncloud/mirall/wiki/Testing-Scenarios
* [ ] Make sure to have `client/ChangeLog` updated
* use `git log --format=oneline v<lastrelease>...master` if your memory fails you
* [ ] check if enterprise issues are fixed
One week before the release:
* [ ] Communicate the release schedule on mailinglist release-coordination@owncloud.com. Give a high level overview of the upcoming new features, changes etc.
* [ ] Ensure marketing is aware (marketing@owncloud.com) and prepared for the release (social, .com website, cust. communications)
* [ ] Inform GCX knows the next version is about 1 week out (gcx@owncloud.com)
For all Betas and RCs:
* [ ] Branch off a release branch called VERSION-rcX or VERSION-betaX (without v, v is for tags)
* [ ] Edit ```VERSION.cmake``` to set the suffix to beta1, beta2 etc. Commit the result to the release branch only
* [ ] Create build for Windows using rotor job owncloud-client-win32 (uncheck the "nightly build" checkbox, check the "sign package" checkboxes) both themes 'ownCloud' and 'testpilotcould'
* [ ] Create build for Mac using rotor, job owncloud-client-osx (uncheck the "nightly build" checkbox, check the "sign package" checkboxes) both themes 'ownCloud' and 'testpilotcould'
* [ ] Create the beta tarball using Jenkins job ownCloud-client-source
* [ ] Create Linux builds using rotor job owncloud-client-linux building (this magically interacts with the ownCloud-client-source job)
* [ ] theme 'ownCloud' -> isv:ownCloud:community:testing
* [ ] theme 'testpilotcould' -> isv:ownCloud:testpilot:testing
* [ ] Copy builds from ```daily``` to ```testing``` on download.owncloud.com, double check the download links.
* [ ] Create a pull request to the owncloud.org repository to update the install page (strings.php, page-desktop.php) and the changelog on owncloud.org. From now on download packages from the staging webserver.
* [ ] Inform community mailinglists devel@owncloud.org and testpilots@owncloud.org
* [ ] Announce on https://central.owncloud.org
* [ ] Create a signed tag using ```git tag -u E94E7B37 tagname``` (https://github.com/owncloud/enterprise/wiki/Desktop-Signing-Knowledge)
* [ ] Check crash reporter
For first Beta of a Major or Minor release:
* [ ] branch off master to new version branch (e.g. master -> 2.1, when releasing 2.1)
* [ ] Adjust `VERSION.cmake` in master and count up (e.g. 2.2)
* [ ] Adjust translation jobs for [client](https://ci.owncloud.org/view/translation-sync/job/translation-sync-client/) and [NSIS](https://ci.owncloud.org/view/translation-sync/job/translation-sync-client-nsis/) to point to the release branch (e.g. 2.1).
* [ ] Make sure there is a job for the docs of the new master branch and the current release branch on rotor.
Day before Release:
* [ ] Check the translations coming from transifex: All synchronized?
* [ ] Run the tx.pl scripts on the final code tag
* [ ] Run ```make test```
* [ ] Run smashbox on the final code tag
* [ ] Inform product management and marketing that we are 1 day out
On Release Day (for final release):
* [ ] Branch off a release branch called like the version (without v, v is for tags)
* [ ] Double check ```VERSION.cmake```: Check the version number settings and suffix (beta etc.) to be removed. Commit change to release branch only!
* [ ] Make sure to increase the version number of the branched of release, e.g. if you release 2.3.2 then you should change VERSION.cmake in 2.3 to 2.3.3 since that branch now will be 2.3.3
* [ ] Add last updates to Changelog in the client source repository.
* [ ] Create tar ball (automated by `ownCloud-client-source` jenkins job) and **immediately** sign it (asc file). (https://github.com/owncloud/enterprise/wiki/Desktop-Signing-Knowledge)
* [ ] Create build for Windows using rotor job owncloud-client-win32 (uncheck the "nightly build" checkbox, check the "sign package" checkboxes) both themes 'ownCloud' and 'testpilotcould'
* [ ] Create build for Mac using rotor, job owncloud-client-osx (uncheck the "nightly build" checkbox, check the "sign package" checkboxes) both themes 'ownCloud' and 'testpilotcould'
* [ ] Stop publishing on OBS (if still enabled).
* [ ] Branch isv:ownCloud:desktop to isv:ownCloud:desktop:client-X.Y.Z before overwriting https://github.com/owncloud/administration/blob/master/jenkins/obs_integration/obs-backup-prj.sh
* [ ] Create Linux builds using rotor job owncloud-client-linux (this magically interacts with the ownCloud-client-source job)
* Check if patches still apply in the linux packages
* Update [OBS repository](https://build.opensuse.org/project/show?project=isv%3AownCloud%3Adesktop) `isv:ownCloud:desktop`
* [ ] theme 'ownCloud' -> isv:ownCloud:desktop
* [ ] theme 'testpilotcloud' -> isv:ownCloud:testpilot
* [ ] Linux: Update the testing repository to the latest stable version.
* [ ] Inform GCX that a new tarball is available.
* [ ] Copy builds and source tar ball from ```daily``` to ```stable``` on download.owncloud.com, double check the download links.
* [ ] Check if the following packages are on download.owncloud.com/desktop/stable:
* Windows binary package
* Mac binary package
* source tarballs
* [ ] Create a pull request to the owncloud.org repository to update the install page (strings.php, page-desktop.php) and the changelog on owncloud.org. From now on download packages from the staging webserver.
* [ ] Re-download Mac builds and check signature. Interactive in installer window
* [ ] Re-download Win build check signature. From Mac or Linux: ```osslsigncode verify ownCloud-version-setup.exe```
* [ ] Mac: Perform smoke test (Install, make sure it does not explode, and check if all version indicators are correct)
* [ ] Win: Perform smoke test (Install, make sure it does not explode, and check if all version indicators are correct)
* [ ] Linux: Smoke test
* [ ] Linux: Re-enable OBS publishing
* Let obs build and publish exactly once. then
* [ ] disable publishing and rebuild for the owncloud-client package and all its dependencies.
* [ ] double-check that there are no _aggregatepac from other projects, if so disable rebuilding there too.
* [ ] Update ASCII Changelog on http://download.owncloud.com/download/changelog-client
* [ ] Announce on https://central.owncloud.org
* [ ] Announce on announcements@owncloud.org
* [ ] Create git signed tag in client repository using ```git tag -u E94E7B37 tagname```
* [ ] Send out Social (tweet, blog, other)
* [ ] Send out customer communication (if any)
* [ ] Inform GCX that the new version is released (gcx@owncloud.com)
* [ ] Inform release-coordination@owncloud.com
* [ ] Ensure marketing is aware (marketing@owncloud.com)
* [ ] Take pride and celebrate!
* [ ] Also update the testpilotcloud builds for that release version and make sure they show up on the download page
* [ ] Tell GCX to increment the minimum supported version for enterprise customers
* [ ] Check if minimum.supported.desktop.version (https://github.com/owncloud/core/blob/master/config/config.sample.php#L1152) needs to be updated in server
15 minutes after after release:
* [ ] Test all advertised download links to have the expected version
* [ ] Check for build errors in OBS
* [ ] disable publishing in OBS to prevent that accidential rebuilds hit the end users.
A few days after the release (for final release)
* [ ] Review changes in the release branch, merge back into master.
* [ ] Update the updater script ```clientupdater.php``` (check the crash reporter if auto update is a good idea or we need a new release)
* [ ] Execute announced deprecations. Disable builds for deprecated platforms. Update accordingly: https://doc.owncloud.org/server/latest/admin_manual/installation/system_requirements.html#desktop
* [ ] Increment version number in nightly builds. Special case: after the last release in a branch, jump forward to the 'next release branch'... That may mean, this is nightly is the same as edge then.
```

View file

@ -32,6 +32,7 @@
#include "common/syncjournalfilerecord.h"
#include "elidedlabel.h"
#include "ui_issueswidget.h"
#include <climits>
@ -54,6 +55,9 @@ IssuesWidget::IssuesWidget(QWidget *parent)
connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &IssuesWidget::slotOpenFile);
connect(_ui->copyIssuesButton, &QAbstractButton::clicked, this, &IssuesWidget::copyToClipboard);
_ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu);
connect(_ui->showIgnores, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
connect(_ui->showWarnings, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues);
@ -85,7 +89,7 @@ IssuesWidget::IssuesWidget(QWidget *parent)
_ui->_treeWidget->setHeaderLabels(header);
int timestampColumnWidth =
ActivityItemDelegate::rowHeight() // icon
+ _ui->_treeWidget->fontMetrics().width(ProtocolWidget::timeString(QDateTime::currentDateTime()))
+ _ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime()))
+ timestampColumnExtra;
_ui->_treeWidget->setColumnWidth(0, timestampColumnWidth);
_ui->_treeWidget->setColumnWidth(1, 180);
@ -198,7 +202,7 @@ void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPt
{
if (!item->hasErrorStatus())
return;
QTreeWidgetItem *line = ProtocolWidget::createCompletedTreewidgetItem(folder, *item);
QTreeWidgetItem *line = ProtocolItem::create(folder, *item);
if (!line)
return;
addItem(line);
@ -233,6 +237,15 @@ void IssuesWidget::slotAccountRemoved(AccountState *account)
updateAccountChoiceVisibility();
}
void IssuesWidget::slotItemContextMenu(const QPoint &pos)
{
auto item = _ui->_treeWidget->itemAt(pos);
if (!item)
return;
auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos);
ProtocolItem::openContextMenu(globalPos, item, this);
}
void IssuesWidget::updateAccountChoiceVisibility()
{
bool visible = _ui->filterAccount->count() > 2;
@ -373,8 +386,8 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message,
QStringList columns;
QDateTime timestamp = QDateTime::currentDateTime();
const QString timeStr = ProtocolWidget::timeString(timestamp);
const QString longTimeStr = ProtocolWidget::timeString(timestamp, QLocale::LongFormat);
const QString timeStr = ProtocolItem::timeString(timestamp);
const QString longTimeStr = ProtocolItem::timeString(timestamp, QLocale::LongFormat);
columns << timeStr;
columns << ""; // no "File" entry
@ -383,7 +396,7 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message,
QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error);
QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns);
QTreeWidgetItem *twitem = new ProtocolItem(columns);
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setData(0, Qt::UserRole, timestamp);
twitem->setIcon(0, icon);

View file

@ -68,6 +68,7 @@ private slots:
void slotUpdateFolderFilters();
void slotAccountAdded(AccountState *account);
void slotAccountRemoved(AccountState *account);
void slotItemContextMenu(const QPoint &pos);
private:
void updateAccountChoiceVisibility();

View file

@ -25,6 +25,8 @@
#include "folder.h"
#include "openfilemanager.h"
#include "activityitemdelegate.h"
#include "guiutility.h"
#include "accountstate.h"
#include "ui_protocolwidget.h"
@ -32,7 +34,115 @@
namespace OCC {
bool SortedTreeWidgetItem::operator<(const QTreeWidgetItem &other) const
QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format)
{
const QLocale loc = QLocale::system();
QString dtFormat = loc.dateTimeFormat(format);
static const QRegExp re("(HH|H|hh|h):mm(?!:s)");
dtFormat.replace(re, "\\1:mm:ss");
return loc.toString(dt, dtFormat);
}
ProtocolItem *ProtocolItem::create(const QString &folder, const SyncFileItem &item)
{
auto f = FolderMan::instance()->folder(folder);
if (!f) {
return 0;
}
QStringList columns;
QDateTime timestamp = QDateTime::currentDateTime();
const QString timeStr = timeString(timestamp);
const QString longTimeStr = timeString(timestamp, QLocale::LongFormat);
columns << timeStr;
columns << Utility::fileNameForGuiUse(item._originalFile);
columns << f->shortGuiLocalPath();
// If the error string is set, it's prefered because it is a useful user message.
QString message = item._errorString;
if (message.isEmpty()) {
message = Progress::asResultString(item);
}
columns << message;
QIcon icon;
if (item._status == SyncFileItem::NormalError
|| item._status == SyncFileItem::FatalError
|| item._status == SyncFileItem::DetailError
|| item._status == SyncFileItem::BlacklistedError) {
icon = Theme::instance()->syncStateIcon(SyncResult::Error);
} else if (Progress::isWarningKind(item._status)) {
icon = Theme::instance()->syncStateIcon(SyncResult::Problem);
}
if (ProgressInfo::isSizeDependent(item)) {
columns << Utility::octetsToString(item._size);
}
ProtocolItem *twitem = new ProtocolItem(columns);
// Warning: The data and tooltips on the columns define an implicit
// interface and can only be changed with care.
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setData(0, Qt::UserRole, timestamp);
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(1, item._file);
twitem->setData(2, Qt::UserRole, folder);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, item._status);
return twitem;
}
SyncJournalFileRecord ProtocolItem::syncJournalRecord(QTreeWidgetItem *item)
{
SyncJournalFileRecord rec;
auto f = folder(item);
if (!f)
return rec;
f->journalDb()->getFileRecord(item->toolTip(1), &rec);
return rec;
}
Folder *ProtocolItem::folder(QTreeWidgetItem *item)
{
return FolderMan::instance()->folder(item->data(2, Qt::UserRole).toString());
}
void ProtocolItem::openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent)
{
auto f = ProtocolItem::folder(item);
if (!f)
return;
AccountPtr account = f->accountState()->account();
auto rec = ProtocolItem::syncJournalRecord(item);
// rec might not be valid
auto menu = new QMenu(parent);
if (rec.isValid()) {
// "Open in Browser" action
auto openInBrowser = menu->addAction(ProtocolWidget::tr("Open in browser"));
QObject::connect(openInBrowser, &QAction::triggered, parent, [parent, account, rec]() {
fetchPrivateLinkUrl(account, rec._path, rec.numericFileId(), parent,
[parent](const QString &url) {
Utility::openBrowser(url, parent);
});
});
}
// More actions will be conditionally added to the context menu here later
if (menu->actions().isEmpty()) {
delete menu;
return;
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(globalPos);
}
bool ProtocolItem::operator<(const QTreeWidgetItem &other) const
{
int column = treeWidget()->sortColumn();
if (column != 0) {
@ -56,6 +166,9 @@ ProtocolWidget::ProtocolWidget(QWidget *parent)
connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &ProtocolWidget::slotOpenFile);
_ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &ProtocolWidget::slotItemContextMenu);
// Adjust copyToClipboard() when making changes here!
QStringList header;
header << tr("Time");
@ -71,7 +184,7 @@ ProtocolWidget::ProtocolWidget(QWidget *parent)
_ui->_treeWidget->setHeaderLabels(header);
int timestampColumnWidth =
_ui->_treeWidget->fontMetrics().width(timeString(QDateTime::currentDateTime()))
_ui->_treeWidget->fontMetrics().width(ProtocolItem::timeString(QDateTime::currentDateTime()))
+ timestampColumnExtra;
_ui->_treeWidget->setColumnWidth(0, timestampColumnWidth);
_ui->_treeWidget->setColumnWidth(1, 180);
@ -119,14 +232,13 @@ void ProtocolWidget::hideEvent(QHideEvent *ev)
QWidget::hideEvent(ev);
}
QString ProtocolWidget::timeString(QDateTime dt, QLocale::FormatType format)
void ProtocolWidget::slotItemContextMenu(const QPoint &pos)
{
const QLocale loc = QLocale::system();
QString dtFormat = loc.dateTimeFormat(format);
static const QRegExp re("(HH|H|hh|h):mm(?!:s)");
dtFormat.replace(re, "\\1:mm:ss");
return loc.toString(dt, dtFormat);
auto item = _ui->_treeWidget->itemAt(pos);
if (!item)
return;
auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos);
ProtocolItem::openContextMenu(globalPos, item, this);
}
void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int)
@ -144,60 +256,11 @@ void ProtocolWidget::slotOpenFile(QTreeWidgetItem *item, int)
}
}
QTreeWidgetItem *ProtocolWidget::createCompletedTreewidgetItem(const QString &folder, const SyncFileItem &item)
{
auto f = FolderMan::instance()->folder(folder);
if (!f) {
return 0;
}
QStringList columns;
QDateTime timestamp = QDateTime::currentDateTime();
const QString timeStr = timeString(timestamp);
const QString longTimeStr = timeString(timestamp, QLocale::LongFormat);
columns << timeStr;
columns << Utility::fileNameForGuiUse(item._originalFile);
columns << f->shortGuiLocalPath();
// If the error string is set, it's prefered because it is a useful user message.
QString message = item._errorString;
if (message.isEmpty()) {
message = Progress::asResultString(item);
}
columns << message;
QIcon icon;
if (item._status == SyncFileItem::NormalError
|| item._status == SyncFileItem::FatalError
|| item._status == SyncFileItem::DetailError
|| item._status == SyncFileItem::BlacklistedError) {
icon = Theme::instance()->syncStateIcon(SyncResult::Error);
} else if (Progress::isWarningKind(item._status)) {
icon = Theme::instance()->syncStateIcon(SyncResult::Problem);
}
if (ProgressInfo::isSizeDependent(item)) {
columns << Utility::octetsToString(item._size);
}
QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns);
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setData(0, Qt::UserRole, timestamp);
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(1, item._file);
twitem->setData(2, Qt::UserRole, folder);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, item._status);
return twitem;
}
void ProtocolWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
{
if (item->hasErrorStatus())
return;
QTreeWidgetItem *line = createCompletedTreewidgetItem(folder, *item);
QTreeWidgetItem *line = ProtocolItem::create(folder, *item);
if (line) {
// Limit the number of items
int itemCnt = _ui->_treeWidget->topLevelItemCount();

View file

@ -35,16 +35,25 @@ namespace Ui {
class Application;
/**
* A QTreeWidgetItem with special sorting.
* The items used in the protocol and issue QTreeWidget
*
* It allows items for global entries to be moved to the top if the
* Special sorting: It allows items for global entries to be moved to the top if the
* sorting section is the "Time" column.
*/
class SortedTreeWidgetItem : public QTreeWidgetItem
class ProtocolItem : public QTreeWidgetItem
{
public:
using QTreeWidgetItem::QTreeWidgetItem;
// Shared with IssueWidget
static ProtocolItem *create(const QString &folder, const SyncFileItem &item);
static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat);
static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item);
static Folder *folder(QTreeWidgetItem *item);
static void openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent);
private:
bool operator<(const QTreeWidgetItem &other) const override;
};
@ -63,10 +72,6 @@ public:
void storeSyncActivity(QTextStream &ts);
// Shared with IssueWidget
static QTreeWidgetItem *createCompletedTreewidgetItem(const QString &folder, const SyncFileItem &item);
static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat);
public slots:
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
void slotOpenFile(QTreeWidgetItem *item, int);
@ -75,6 +80,9 @@ protected:
void showEvent(QShowEvent *);
void hideEvent(QHideEvent *);
private slots:
void slotItemContextMenu(const QPoint &pos);
signals:
void copyToClipboard();

View file

@ -57,23 +57,6 @@ namespace OCC {
#include "settingsdialogcommon.cpp"
static QIcon circleMask(const QImage &avatar)
{
int dim = avatar.width();
QPixmap fixedImage(dim, dim);
fixedImage.fill(Qt::transparent);
QPainter imgPainter(&fixedImage);
QPainterPath clip;
clip.addEllipse(0, 0, dim, dim);
imgPainter.setClipPath(clip);
imgPainter.drawImage(0, 0, avatar);
imgPainter.end();
return QIcon(fixedImage);
}
//
// Whenever you change something here check both settingsdialog.cpp and settingsdialogmac.cpp !
//
@ -232,7 +215,7 @@ void SettingsDialog::accountAdded(AccountState *s)
accountAction = createColorAwareAction(QLatin1String(":/client/resources/account.png"),
actionText);
} else {
QIcon icon = circleMask(avatar);
QIcon icon(QPixmap::fromImage(AvatarJob::makeCircularAvatar(avatar)));
accountAction = createActionWithIcon(icon, actionText);
}
@ -265,7 +248,7 @@ void SettingsDialog::slotAccountAvatarChanged()
if (action) {
QImage pix = account->avatar();
if (!pix.isNull()) {
action->setIcon(circleMask(pix));
action->setIcon(QPixmap::fromImage(AvatarJob::makeCircularAvatar(pix)));
}
}
}

View file

@ -41,6 +41,9 @@
#include <QAction>
#include <QDesktopServices>
#include <QMessageBox>
#include <QCryptographicHash>
#include <QColor>
#include <QPainter>
namespace OCC {
@ -178,13 +181,12 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
auto newViewPort = new QWidget(scrollArea);
auto layout = new QVBoxLayout(newViewPort);
layout->setMargin(0);
layout->setSpacing(0);
QSize minimumSize = newViewPort->sizeHint();
int x = 0;
if (shares.isEmpty()) {
layout->addWidget(new QLabel(tr("The item is not shared with any users or groups")));
} else {
foreach (const auto &share, shares) {
// We don't handle link shares
if (share->getShareType() == Share::TypeLink) {
@ -194,6 +196,7 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
ShareUserLine *s = new ShareUserLine(share, _maxSharingPermissions, _isFile, _ui->scrollArea);
connect(s, &ShareUserLine::resizeRequested, this, &ShareUserGroupWidget::slotAdjustScrollWidgetSize);
connect(s, &ShareUserLine::visualDeletionDone, this, &ShareUserGroupWidget::getShares);
s->setBackgroundRole(layout->count() % 2 == 0 ? QPalette::Base : QPalette::AlternateBase);
layout->addWidget(s);
x++;
@ -203,6 +206,9 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
minimumSize.rwidth() = qMax(newViewPort->sizeHint().width(), minimumSize.width());
}
}
if (layout->isEmpty()) {
layout->addWidget(new QLabel(tr("The item is not shared with any users or groups")));
} else {
layout->addStretch(1);
}
@ -416,6 +422,65 @@ ShareUserLine::ShareUserLine(QSharedPointer<Share> share,
if (!share->account()->capabilities().shareResharing()) {
_ui->permissionShare->hide();
}
loadAvatar();
}
void ShareUserLine::loadAvatar()
{
const int avatarSize = 36;
// Set size of the placeholder
_ui->avatar->setMinimumHeight(avatarSize);
_ui->avatar->setMinimumWidth(avatarSize);
_ui->avatar->setMaximumHeight(avatarSize);
_ui->avatar->setMaximumWidth(avatarSize);
_ui->avatar->setAlignment(Qt::AlignCenter);
/* Create the fallback avatar.
*
* This will be shown until the avatar image data arrives.
*/
const QByteArray hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5);
double hue = static_cast<quint8>(hash[0]) / 255.;
// See core/js/placeholder.js for details on colors and styling
const QColor bg = QColor::fromHslF(hue, 0.7, 0.68);
const QString style = QString(R"(* {
color: #fff;
background-color: %1;
border-radius: %2px;
text-align: center;
line-height: %2px;
font-size: %2px;
})").arg(bg.name(), QString::number(avatarSize / 2));
_ui->avatar->setStyleSheet(style);
// The avatar label is the first character of the user name.
const QString text = _share->getShareWith()->displayName();
_ui->avatar->setText(text.at(0).toUpper());
/* Start the network job to fetch the avatar data.
*
* Currently only regular users can have avatars.
*/
if (_share->getShareWith()->type() == Sharee::User) {
AvatarJob *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this);
connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded);
job->start();
}
}
void ShareUserLine::slotAvatarLoaded(QImage avatar)
{
if (avatar.isNull())
return;
avatar = AvatarJob::makeCircularAvatar(avatar);
_ui->avatar->setPixmap(QPixmap::fromImage(avatar));
// Remove the stylesheet for the fallback avatar
_ui->avatar->setStyleSheet("");
}
void ShareUserLine::on_deleteShareButton_clicked()

View file

@ -131,8 +131,11 @@ private slots:
void slotShareDeleted();
void slotPermissionsSet();
void slotAvatarLoaded(QImage avatar);
private:
void displayPermissions();
void loadAvatar();
Ui::ShareUserLine *_ui;
QSharedPointer<Share> _share;

View file

@ -10,27 +10,25 @@
<x>0</x>
<y>0</y>
<width>468</width>
<height>64</height>
<height>46</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="avatar">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="sharedWith">
<property name="text">
@ -55,38 +53,26 @@
</spacer>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QCheckBox" name="permissionsEdit">
<property name="text">
<string>can edit</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="permissionShare">
<property name="text">
<string>can share</string>
</property>
</widget>
</item>
<item row="0" column="2">
<item>
<widget class="QCheckBox" name="permissionsEdit">
<property name="text">
<string>can edit</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="permissionToolButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QToolButton" name="deleteShareButton">
<property name="text">

View file

@ -492,7 +492,7 @@ void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listen
}
// Fetches the private link url asynchronously and then calls the target slot
void fetchPrivateLinkUrl(const QString &localFile, SocketApi *target, void (SocketApi::*targetFun)(const QString &url) const)
static void fetchPrivateLinkUrlHelper(const QString &localFile, SocketApi *target, void (SocketApi::*targetFun)(const QString &url) const)
{
Folder *shareFolder = FolderMan::instance()->folderForPath(localFile);
if (!shareFolder) {
@ -505,45 +505,23 @@ void fetchPrivateLinkUrl(const QString &localFile, SocketApi *target, void (Sock
AccountPtr account = shareFolder->accountState()->account();
// Generate private link ourselves: used as a fallback
SyncJournalFileRecord rec;
if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid())
return;
const QString oldUrl =
account->deprecatedPrivateLinkUrl(rec.numericFileId()).toString(QUrl::FullyEncoded);
// Retrieve the new link or numeric file id by PROPFIND
PropfindJob *job = new PropfindJob(account, file, target);
job->setProperties(
QList<QByteArray>()
<< "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000);
QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) {
(target->*targetFun)(privateLinkUrl);
} else if (!numericFileId.isEmpty()) {
(target->*targetFun)(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
} else {
(target->*targetFun)(oldUrl);
}
fetchPrivateLinkUrl(account, file, rec.numericFileId(), target, [=](const QString &url) {
(target->*targetFun)(url);
});
QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) {
(target->*targetFun)(oldUrl);
});
job->start();
}
void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *)
{
fetchPrivateLinkUrl(localFile, this, &SocketApi::copyPrivateLinkToClipboard);
fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::copyPrivateLinkToClipboard);
}
void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *)
{
fetchPrivateLinkUrl(localFile, this, &SocketApi::emailPrivateLink);
fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::emailPrivateLink);
}
void SocketApi::copyPrivateLinkToClipboard(const QString &link) const

View file

@ -335,7 +335,7 @@ void ConnectionValidator::slotUserFetched(const QJsonDocument &json)
if (!user.isEmpty()) {
_account->setDavUser(user);
AvatarJob *job = new AvatarJob(_account, this);
AvatarJob *job = new AvatarJob(_account, _account->davUser(), 128, this);
job->setTimeout(20 * 1000);
QObject::connect(job, &AvatarJob::avatarPixmap, this, &ConnectionValidator::slotAvatarImage);

View file

@ -30,6 +30,8 @@
#include <QPixmap>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
#include "networkjobs.h"
#include "account.h"
#include "owncloudpropagator.h"
@ -629,10 +631,10 @@ bool PropfindJob::finished()
/*********************************************************************************************/
AvatarJob::AvatarJob(AccountPtr account, QObject *parent)
AvatarJob::AvatarJob(AccountPtr account, const QString &userId, int size, QObject *parent)
: AbstractNetworkJob(account, QString(), parent)
{
_avatarUrl = Utility::concatUrlPath(account->url(), QString("remote.php/dav/avatars/%1/128.png").arg(account->davUser()));
_avatarUrl = Utility::concatUrlPath(account->url(), QString("remote.php/dav/avatars/%1/%2.png").arg(userId, QString::number(size)));
}
void AvatarJob::start()
@ -642,6 +644,26 @@ void AvatarJob::start()
AbstractNetworkJob::start();
}
QImage AvatarJob::makeCircularAvatar(const QImage &baseAvatar)
{
int dim = baseAvatar.width();
QImage avatar(dim, dim, baseAvatar.format());
avatar.fill(Qt::transparent);
QPainter painter(&avatar);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addEllipse(0, 0, dim, dim);
painter.setClipPath(path);
painter.drawImage(0, 0, baseAvatar);
painter.end();
return avatar;
}
bool AvatarJob::finished()
{
int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -908,6 +930,7 @@ bool SimpleNetworkJob::finished()
return true;
}
DeleteApiJob::DeleteApiJob(AccountPtr account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent)
{
@ -942,5 +965,37 @@ bool DeleteApiJob::finished()
return true;
}
void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath,
const QByteArray &numericFileId, QObject *target,
std::function<void(const QString &url)> targetFun)
{
QString oldUrl;
if (!numericFileId.isEmpty())
oldUrl = account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
// Retrieve the new link by PROPFIND
PropfindJob *job = new PropfindJob(account, remotePath, target);
job->setProperties(
QList<QByteArray>()
<< "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000);
QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) {
targetFun(privateLinkUrl);
} else if (!numericFileId.isEmpty()) {
targetFun(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
} else {
targetFun(oldUrl);
}
});
QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) {
targetFun(oldUrl);
});
job->start();
}
} // namespace OCC

View file

@ -19,6 +19,7 @@
#include "abstractnetworkjob.h"
#include <QBuffer>
#include <functional>
class QUrl;
class QJsonObject;
@ -165,9 +166,7 @@ private:
/**
* @brief The AvatarJob class
*
* Retrieves the account users avatar from the server using a GET request.
* @brief Retrieves the account users avatar from the server using a GET request.
*
* If the server does not have the avatar, the result Pixmap is empty.
*
@ -177,9 +176,17 @@ class OWNCLOUDSYNC_EXPORT AvatarJob : public AbstractNetworkJob
{
Q_OBJECT
public:
explicit AvatarJob(AccountPtr account, QObject *parent = 0);
/**
* @param userId The user for which to obtain the avatar
* @param size The size of the avatar (square so size*size)
*/
explicit AvatarJob(AccountPtr account, const QString &userId, int size, QObject *parent = 0);
void start() Q_DECL_OVERRIDE;
/** The retrieved avatar images don't have the circle shape by default */
static QImage makeCircularAvatar(const QImage &baseAvatar);
signals:
/**
* @brief avatarPixmap - returns either a valid pixmap or not.
@ -432,5 +439,23 @@ private slots:
bool finished() Q_DECL_OVERRIDE;
};
}
/**
* @brief Runs a PROPFIND to figure out the private link url
*
* The numericFileId is used only to build the deprecatedPrivateLinkUrl
* locally as a fallback. If it's empty and the PROPFIND fails, targetFun
* will be called with an empty string.
*
* The job and signal connections are parented to the target QObject.
*
* Note: targetFun is guaranteed to be called only through the event
* loop and never directly.
*/
void OWNCLOUDSYNC_EXPORT fetchPrivateLinkUrl(
AccountPtr account, const QString &remotePath,
const QByteArray &numericFileId, QObject *target,
std::function<void(const QString &url)> targetFun);
} // namespace OCC
#endif // NETWORKJOBS_H