Enable share to Talk and Email. Display correct icon. Added unit tests.

Signed-off-by: allexzander <blackslayer4@gmail.com>
This commit is contained in:
allexzander 2021-08-17 16:39:18 +03:00 committed by allexzander (Rebase PR Action)
parent 3f6defe594
commit a3fc812539
16 changed files with 306 additions and 67 deletions

View file

@ -149,4 +149,4 @@ trigger:
- master
event:
- pull_request
- push
- push

View file

@ -104,6 +104,7 @@ set(client_SRCS
elidedlabel.cpp
headerbanner.cpp
iconjob.cpp
iconutils.cpp
remotewipe.cpp
tray/ActivityData.cpp
tray/ActivityListModel.cpp

92
src/gui/iconutils.cpp Normal file
View file

@ -0,0 +1,92 @@
#include "iconutils.h"
#include <theme.h>
#include <QFile>
#include <QPainter>
#include <QPixmapCache>
#include <QSvgRenderer>
namespace OCC {
namespace Ui {
namespace IconUtils {
QPixmap pixmapForBackground(const QString &fileName, const QColor &backgroundColor)
{
Q_ASSERT(!fileName.isEmpty());
// some icons are present in white or black only, so, we need to check both when needed
const auto iconBaseColors = QStringList({ QStringLiteral("black"), QStringLiteral("white") });
const QString pixmapColor = backgroundColor.isValid() && !Theme::isDarkColor(backgroundColor) ? "black" : "white";
const QString cacheKey = fileName + QLatin1Char(',') + pixmapColor;
QPixmap cachedPixmap;
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
if (iconBaseColors.contains(pixmapColor)) {
cachedPixmap = QPixmap::fromImage(QImage(QString(Theme::themePrefix) + pixmapColor + QLatin1Char('/') + fileName));
QPixmapCache::insert(cacheKey, cachedPixmap);
return cachedPixmap;
}
const auto drawSvgWithCustomFillColor = [](const QString &sourceSvgPath, const QString &fillColor) {
QSvgRenderer svgRenderer;
if (!svgRenderer.load(sourceSvgPath)) {
return QPixmap();
}
// render source image
QImage svgImage(svgRenderer.defaultSize(), QImage::Format_ARGB32);
{
QPainter svgImagePainter(&svgImage);
svgImage.fill(Qt::GlobalColor::transparent);
svgRenderer.render(&svgImagePainter);
}
// draw target image with custom fillColor
QImage image(svgRenderer.defaultSize(), QImage::Format_ARGB32);
image.fill(QColor(fillColor));
{
QPainter imagePainter(&image);
imagePainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
imagePainter.drawImage(0, 0, svgImage);
}
return QPixmap::fromImage(image);
};
// find the first matching svg among base colors, if any
const QString sourceSvg = [&]() {
for (const auto &color : iconBaseColors) {
const QString baseSVG(QString(Theme::themePrefix) + color + QLatin1Char('/') + fileName);
if (QFile(baseSVG).exists()) {
return baseSVG;
}
}
return QString();
}();
Q_ASSERT(!sourceSvg.isEmpty());
if (sourceSvg.isEmpty()) {
qWarning("Failed to find base svg for %s", qPrintable(cacheKey));
return {};
}
cachedPixmap = drawSvgWithCustomFillColor(sourceSvg, pixmapColor);
QPixmapCache::insert(cacheKey, cachedPixmap);
Q_ASSERT(!cachedPixmap.isNull());
if (cachedPixmap.isNull()) {
qWarning("Failed to load pixmap for %s", qPrintable(cacheKey));
return {};
}
}
return cachedPixmap;
}
}
}
}

28
src/gui/iconutils.h Normal file
View file

@ -0,0 +1,28 @@
/*
* 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.
*/
#ifndef ICONUTILS_H
#define ICONUTILS_H
#include <QColor>
#include <QPixmap>
namespace OCC {
namespace Ui {
namespace IconUtils {
QPixmap pixmapForBackground(const QString &fileName, const QColor &backgroundColor);
}
}
}
#endif // ICONUTILS_H

View file

@ -296,7 +296,7 @@ UserGroupShare::UserGroupShare(AccountPtr account,
, _note(note)
, _expireDate(expireDate)
{
Q_ASSERT(shareType == TypeUser || shareType == TypeGroup || shareType == TypeEmail);
Q_ASSERT(shareType == TypeUser || shareType == TypeGroup || shareType == TypeEmail || shareType == TypeRoom);
Q_ASSERT(shareWith);
}
@ -326,6 +326,11 @@ QDate UserGroupShare::getExpireDate() const
void UserGroupShare::setExpireDate(const QDate &date)
{
if (_expireDate == date) {
emit expireDateSet();
return;
}
auto *job = new OcsShareJob(_account);
connect(job, &OcsShareJob::shareJobFinished, this, &UserGroupShare::slotExpireDateSet);
connect(job, &OcsJob::ocsError, this, &UserGroupShare::slotOcsError);
@ -461,7 +466,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
if (shareType == Share::TypeLink) {
newShare = parseLinkShare(data);
} else if (shareType == Share::TypeGroup || shareType == Share::TypeUser || shareType == Share::TypeEmail) {
} else if (shareType == Share::TypeGroup || shareType == Share::TypeUser || shareType == Share::TypeEmail || shareType == Share::TypeRoom) {
newShare = parseUserGroupShare(data);
} else {
newShare = parseShare(data);

View file

@ -27,6 +27,7 @@
#include "thumbnailjob.h"
#include "sharemanager.h"
#include "theme.h"
#include "iconutils.h"
#include "QProgressIndicator.h"
#include <QBuffer>
@ -46,6 +47,7 @@
#include <QColor>
#include <QPainter>
#include <QListWidget>
#include <QSvgRenderer>
#include <cstring>
@ -247,7 +249,7 @@ void ShareUserGroupWidget::slotSharesFetched(const QList<QSharedPointer<Share>>
}
Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup || share->getShareType() == Share::TypeEmail);
Q_ASSERT(share->getShareType() == Share::TypeUser || share->getShareType() == Share::TypeGroup || share->getShareType() == Share::TypeEmail || share->getShareType() == Share::TypeRoom);
auto userGroupShare = qSharedPointerDynamicCast<UserGroupShare>(share);
auto *s = new ShareUserLine(_account, userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea);
connect(s, &ShareUserLine::resizeRequested, this, &ShareUserGroupWidget::slotAdjustScrollWidgetSize);
@ -501,7 +503,6 @@ ShareUserLine::ShareUserLine(AccountPtr account,
_ui->permissionsEdit->setEnabled(enabled);
connect(_ui->permissionsEdit, &QAbstractButton::clicked, this, &ShareUserLine::slotEditPermissionsChanged);
connect(_ui->noteConfirmButton, &QAbstractButton::clicked, this, &ShareUserLine::onNoteConfirmButtonClicked);
connect(_ui->confirmExpirationDate, &QAbstractButton::clicked, this, &ShareUserLine::setExpireDate);
connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareUserLine::setExpireDate);
connect(_share.data(), &UserGroupShare::noteSet, this, &ShareUserLine::disableProgessIndicatorAnimation);
@ -521,10 +522,9 @@ ShareUserLine::ShareUserLine(AccountPtr account,
showNoteOptions(false);
// email shares do not support notes and expiration dates
const bool isNoteAndExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
const bool isNoteSupported = _share->getShareType() != Share::ShareType::TypeEmail && _share->getShareType() != Share::ShareType::TypeRoom;
if (isNoteAndExpirationDateSupported) {
if (isNoteSupported) {
_noteLinkAction = new QAction(tr("Note to recipient"));
_noteLinkAction->setCheckable(true);
menu->addAction(_noteLinkAction);
@ -537,7 +537,9 @@ ShareUserLine::ShareUserLine(AccountPtr account,
showExpireDateOptions(false);
if (isNoteAndExpirationDateSupported) {
const bool isExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail;
if (isExpirationDateSupported) {
// email shares do not support expiration dates
_expirationDateLinkAction = new QAction(tr("Set expiration date"));
_expirationDateLinkAction->setCheckable(true);
@ -545,9 +547,8 @@ ShareUserLine::ShareUserLine(AccountPtr account,
connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions);
const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate();
if (!expireDate.isNull()) {
_ui->calendar->setDate(expireDate);
_expirationDateLinkAction->setChecked(true);
showExpireDateOptions(true);
showExpireDateOptions(true, expireDate);
}
}
@ -646,28 +647,7 @@ void ShareUserLine::loadAvatar()
_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());
setDefaultAvatar(avatarSize);
/* Start the network job to fetch the avatar data.
*
@ -680,6 +660,38 @@ void ShareUserLine::loadAvatar()
}
}
void ShareUserLine::setDefaultAvatar(int avatarSize)
{
/* Create the fallback avatar.
*
* This will be shown until the avatar image data arrives.
*/
// See core/js/placeholder.js for details on colors and styling
const auto backgroundColor = backgroundColorForShareeType(_share->getShareWith()->type());
const QString style = QString(R"(* {
color: #fff;
background-color: %1;
border-radius: %2px;
text-align: center;
line-height: %2px;
font-size: %2px;
})").arg(backgroundColor.name(), QString::number(avatarSize / 2));
_ui->avatar->setStyleSheet(style);
const auto pixmap = pixmapForShareeType(_share->getShareWith()->type(), backgroundColor);
if (!pixmap.isNull()) {
_ui->avatar->setPixmap(pixmap);
} else {
qCDebug(lcSharing) << "pixmap is null for share type: " << _share->getShareWith()->type();
// The avatar label is the first character of the user name.
const auto text = _share->getShareWith()->displayName();
_ui->avatar->setText(text.at(0).toUpper());
}
}
void ShareUserLine::slotAvatarLoaded(QImage avatar)
{
if (avatar.isNull())
@ -926,13 +938,57 @@ void ShareUserLine::customizeStyle()
_deleteShareButton->setIcon(deleteicon);
_ui->noteConfirmButton->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
_ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg"));
_ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::WindowText));
// make sure to force BackgroundRole to QPalette::WindowText for a lable, because it's parent always has a different role set that applies to children unless customized
_ui->errorLabel->setBackgroundRole(QPalette::WindowText);
}
QPixmap ShareUserLine::pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor) const
{
switch (type) {
case Sharee::Room:
return Ui::IconUtils::pixmapForBackground(QStringLiteral("talk-app.svg"), backgroundColor);
case Sharee::Email:
return Ui::IconUtils::pixmapForBackground(QStringLiteral("email.svg"), backgroundColor);
case Sharee::Group:
case Sharee::Federated:
case Sharee::Circle:
case Sharee::User:
break;
}
return {};
}
QColor ShareUserLine::backgroundColorForShareeType(Sharee::Type type) const
{
switch (type) {
case Sharee::Room:
return Theme::instance()->wizardHeaderBackgroundColor();
case Sharee::Email:
return Theme::instance()->wizardHeaderTitleColor();
case Sharee::Group:
case Sharee::Federated:
case Sharee::Circle:
case Sharee::User:
break;
}
const auto calculateBackgroundBasedOnText = [this]() {
const auto hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5);
Q_ASSERT(hash.size() > 0);
if (hash.size() == 0) {
qCWarning(lcSharing) << "Failed to calculate hash color for share:" << _share->path();
return QColor{};
}
const double hue = static_cast<quint8>(hash[0]) / 255.;
return QColor::fromHslF(hue, 0.7, 0.68);
};
return calculateBackgroundBasedOnText();
}
void ShareUserLine::showNoteOptions(bool show)
{
_ui->noteLabel->setVisible(show);
@ -979,16 +1035,14 @@ void ShareUserLine::toggleExpireDateOptions(bool enable)
}
}
void ShareUserLine::showExpireDateOptions(bool show)
void ShareUserLine::showExpireDateOptions(bool show, const QDate &initialDate)
{
_ui->expirationLabel->setVisible(show);
_ui->calendar->setVisible(show);
_ui->confirmExpirationDate->setVisible(show);
if (show) {
const QDate date = QDate::currentDate().addDays(1);
_ui->calendar->setDate(date);
_ui->calendar->setMinimumDate(date);
_ui->calendar->setMinimumDate(QDate::currentDate().addDays(1));
_ui->calendar->setDate(initialDate.isValid() ? initialDate : _ui->calendar->minimumDate());
_ui->calendar->setFocus();
}

View file

@ -169,15 +169,19 @@ private slots:
private:
void displayPermissions();
void loadAvatar();
void setDefaultAvatar(int avatarSize);
void customizeStyle();
QPixmap pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor = QColor()) const;
QColor backgroundColorForShareeType(Sharee::Type type) const;
void showNoteOptions(bool show);
void toggleNoteOptions(bool enable);
void onNoteConfirmButtonClicked();
void setNote(const QString &note);
void toggleExpireDateOptions(bool enable);
void showExpireDateOptions(bool show);
void showExpireDateOptions(bool show, const QDate &initialDate = QDate());
void setExpireDate();
void togglePasswordSetProgressAnimation(bool show);

View file

@ -273,20 +273,6 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="confirmExpirationDate">
<property name="text">
<string notr="true">…</string>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>

View file

@ -197,7 +197,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray) const
return cached = QIcon::fromTheme(name);
}
const auto svgName = QString::fromLatin1(":/client/theme/%1/%2.svg").arg(flavor).arg(name);
const QString svgName = QString(Theme::themePrefix) + QString::fromLatin1("%1/%2.svg").arg(flavor).arg(name);
QSvgRenderer renderer(svgName);
const auto createPixmapFromSvg = [&renderer] (int size) {
QImage img(size, size, QImage::Format_ARGB32);
@ -208,7 +208,7 @@ QIcon Theme::themeIcon(const QString &name, bool sysTray) const
};
const auto loadPixmap = [flavor, name] (int size) {
const auto pixmapName = QString::fromLatin1(":/client/theme/%1/%2-%3.png").arg(flavor).arg(name).arg(size);
const QString pixmapName = QString(Theme::themePrefix) + QString::fromLatin1("%1/%2-%3.png").arg(flavor).arg(name).arg(size);
return QPixmap(pixmapName);
};
@ -249,8 +249,8 @@ QString Theme::themeImagePath(const QString &name, int size, bool sysTray) const
// branded client may have several sizes of the same icon
const QString filePath = (useSvg || size <= 0)
? QString::fromLatin1(":/client/theme/%1/%2").arg(flavor).arg(name)
: QString::fromLatin1(":/client/theme/%1/%2-%3").arg(flavor).arg(name).arg(size);
? QString(Theme::themePrefix) + QString::fromLatin1("%1/%2").arg(flavor).arg(name)
: QString(Theme::themePrefix) + QString::fromLatin1("%1/%2-%3").arg(flavor).arg(name).arg(size);
const QString svgPath = filePath + ".svg";
if (useSvg) {
@ -274,8 +274,7 @@ bool Theme::isHidpi(QPaintDevice *dev)
QIcon Theme::uiThemeIcon(const QString &iconName, bool uiHasDarkBg) const
{
QString themeResBasePath = ":/client/theme/";
QString iconPath = themeResBasePath + (uiHasDarkBg?"white/":"black/") + iconName;
QString iconPath = QString(Theme::themePrefix) + (uiHasDarkBg ? "white/" : "black/") + iconName;
std::string icnPath = iconPath.toUtf8().constData();
return QIcon(QPixmap(iconPath));
}
@ -303,8 +302,7 @@ QString Theme::hidpiFileName(const QString &iconName, const QColor &backgroundCo
{
const auto isDarkBackground = Theme::isDarkColor(backgroundColor);
const QString themeResBasePath = ":/client/theme/";
const QString iconPath = themeResBasePath + (isDarkBackground ? "white/" : "black/") + iconName;
const QString iconPath = QString(Theme::themePrefix) + (isDarkBackground ? "white/" : "black/") + iconName;
return Theme::hidpiFileName(iconPath, dev);
}
@ -406,7 +404,7 @@ bool Theme::systrayUseMonoIcons() const
bool Theme::monoIconsAvailable() const
{
QString themeDir = QString::fromLatin1(":/client/theme/%1/").arg(Theme::instance()->systrayIconFlavor(true));
QString themeDir = QString(Theme::themePrefix) + QString::fromLatin1("%1/").arg(Theme::instance()->systrayIconFlavor(true));
return QDir(themeDir).exists();
}
@ -510,7 +508,7 @@ QVariant Theme::customMedia(CustomMediaType type)
break;
}
QString imgPath = QString::fromLatin1(":/client/theme/colored/%1.png").arg(key);
QString imgPath = QString(Theme::themePrefix) + QString::fromLatin1("colored/%1.png").arg(key);
if (QFile::exists(imgPath)) {
QPixmap pix(imgPath);
if (pix.isNull()) {
@ -581,11 +579,11 @@ QColor Theme::wizardHeaderBackgroundColor() const
QPixmap Theme::wizardApplicationLogo() const
{
if (!Theme::isBranded()) {
return QPixmap(Theme::hidpiFileName(":/client/theme/colored/wizard-nextcloud.png"));
return QPixmap(Theme::hidpiFileName(QString(Theme::themePrefix) + "colored/wizard-nextcloud.png"));
}
#ifdef APPLICATION_WIZARD_USE_CUSTOM_LOGO
const auto useSvg = shouldPreferSvg();
const auto logoBasePath = QStringLiteral(":/client/theme/colored/wizard_logo");
const QString logoBasePath = QString(Theme::themePrefix) + QStringLiteral("colored/wizard_logo");
if (useSvg) {
const auto maxHeight = Theme::isHidpi() ? 200 : 100;
const auto maxWidth = 2 * maxHeight;
@ -605,7 +603,7 @@ QPixmap Theme::wizardHeaderLogo() const
{
#ifdef APPLICATION_WIZARD_USE_CUSTOM_LOGO
const auto useSvg = shouldPreferSvg();
const auto logoBasePath = QStringLiteral(":/client/theme/colored/wizard_logo");
const QString logoBasePath = QString(Theme::themePrefix) + QStringLiteral("colored/wizard_logo");
if (useSvg) {
const auto maxHeight = 64;
const auto maxWidth = 2 * maxHeight;

View file

@ -535,6 +535,8 @@ public:
*/
virtual bool showVirtualFilesOption() const;
static constexpr const char *themePrefix = ":/client/theme/";
protected:
#ifndef TOKEN_AUTH_ONLY
QIcon themeIcon(const QString &name, bool sysTray = false) const;

View file

@ -57,6 +57,7 @@ nextcloud_add_test(FolderWatcher)
nextcloud_add_test(Capabilities)
nextcloud_add_test(PushNotifications)
nextcloud_add_test(Theme)
nextcloud_add_test(IconUtils)
nextcloud_add_test(NotificationCache)
if( UNIX AND NOT APPLE )

64
test/testiconutils.cpp Normal file
View file

@ -0,0 +1,64 @@
/*
* 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 <QTest>
#include "theme.h"
#include "iconutils.h"
class TestIconUtils : public QObject
{
Q_OBJECT
public:
TestIconUtils()
{
Q_INIT_RESOURCE(resources);
Q_INIT_RESOURCE(theme);
}
private slots:
void testPixmapForBackground()
{
const QDir blackSvgDir(QString(OCC::Theme::themePrefix) + QStringLiteral("black"));
const QStringList blackImages = blackSvgDir.entryList(QStringList("*.svg"));
const QDir whiteSvgDir(QString(OCC::Theme::themePrefix) + QStringLiteral("white"));
const QStringList whiteImages = whiteSvgDir.entryList(QStringList("*.svg"));
if (blackImages.size() > 0) {
// white pixmap for dark background - should not fail
QVERIFY(!OCC::Ui::IconUtils::pixmapForBackground(whiteImages.at(0), QColor("blue")).isNull());
}
if (whiteImages.size() > 0) {
// black pixmap for bright background - should not fail
QVERIFY(!OCC::Ui::IconUtils::pixmapForBackground(blackImages.at(0), QColor("yellow")).isNull());
}
const auto blackImagesExclusive = QSet<QString>(blackImages.begin(), blackImages.end()).subtract(QSet<QString>(whiteImages.begin(), whiteImages.end()));
const auto whiteImagesExclusive = QSet<QString>(whiteImages.begin(), whiteImages.end()).subtract(QSet<QString>(blackImages.begin(), blackImages.end()));
if (blackImagesExclusive != whiteImagesExclusive) {
// black pixmap for dark background - should fail as we don't have this image in black
QVERIFY(OCC::Ui::IconUtils::pixmapForBackground(blackImagesExclusive.values().at(0), QColor("blue")).isNull());
// white pixmap for bright background - should fail as we don't have this image in white
QVERIFY(OCC::Ui::IconUtils::pixmapForBackground(whiteImagesExclusive.values().at(0), QColor("yellow")).isNull());
}
}
};
QTEST_MAIN(TestIconUtils)
#include "testiconutils.moc"

View file

@ -16,6 +16,7 @@
#include "theme.h"
#include "themeutils.h"
#include "iconutils.h"
class TestTheme : public QObject
{

View file

@ -198,5 +198,6 @@
<file>theme/colored/user-status-invisible.svg</file>
<file>theme/colored/user-status-away.svg</file>
<file>theme/colored/user-status-dnd.svg</file>
<file>theme/black/email.svg</file>
</qresource>
</RCC>

View file

@ -198,5 +198,6 @@
<file>theme/colored/user-status-invisible.svg</file>
<file>theme/colored/user-status-away.svg</file>
<file>theme/colored/user-status-dnd.svg</file>
<file>theme/black/email.svg</file>
</qresource>
</RCC>

1
theme/black/email.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 24 24" width="16px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>

After

Width:  |  Height:  |  Size: 267 B