Merge pull request #3182 from nextcloud/bugfix/user-status

Bugfix/user status
This commit is contained in:
Camila 2021-05-25 09:45:05 +02:00 committed by GitHub
commit 5d2cfd8429
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 128 deletions

View file

@ -142,6 +142,11 @@ QUrl AccountState::statusIcon() const
return _userStatus->icon();
}
QString AccountState::statusEmoji() const
{
return _userStatus->emoji();
}
QString AccountState::stateString(State state)
{
switch (state) {
@ -229,7 +234,12 @@ bool AccountState::isDesktopNotificationsAllowed() const
void AccountState::setDesktopNotificationsAllowed(bool isAllowed)
{
if (_isDesktopNotificationsAllowed == isAllowed) {
return;
}
_isDesktopNotificationsAllowed = isAllowed;
emit desktopNotificationsAllowedChanged();
}
void AccountState::checkConnectivity()

View file

@ -167,13 +167,17 @@ public:
*/
UserStatus::Status status() const;
/** Returns the user status Message (emoji + text)
/** Returns the user status Message (text)
*/
QString statusMessage() const;
/** Returns the user status icon url
*/
QUrl statusIcon() const;
/** Returns the user status emoji
*/
QString statusEmoji() const;
/** Returns the notifications status retrieved by the notificatons endpoint
* https://github.com/nextcloud/desktop/issues/2318#issuecomment-680698429
@ -202,6 +206,7 @@ signals:
void isConnectedChanged();
void hasFetchedNavigationApps();
void statusChanged();
void desktopNotificationsAllowedChanged();
protected Q_SLOTS:
void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors);

View file

@ -64,14 +64,16 @@ MenuItem {
spacing: 0
Image {
id: accountAvatar
Layout.leftMargin: 4
Layout.leftMargin: 7
verticalAlignment: Qt.AlignCenter
cache: false
source: model.avatar != "" ? model.avatar : "image://avatars/fallbackBlack"
Layout.preferredHeight: (userLineLayout.height -16)
Layout.preferredWidth: (userLineLayout.height -16)
Layout.preferredHeight: Style.accountAvatarSize
Layout.preferredWidth: Style.accountAvatarSize
Rectangle {
id: accountStatusIndicatorBackground
visible: model.isConnected &&
model.serverHasUserStatus
width: accountStatusIndicator.sourceSize.width + 2
height: width
anchors.bottom: accountAvatar.bottom
@ -81,7 +83,9 @@ MenuItem {
}
Image {
id: accountStatusIndicator
source: model.statusIcon
visible: model.isConnected &&
model.serverHasUserStatus
source: model.statusIcon
cache: false
x: accountStatusIndicatorBackground.x + 1
y: accountStatusIndicatorBackground.y + 1
@ -89,39 +93,63 @@ MenuItem {
sourceSize.height: Style.accountAvatarStateIndicatorSize
Accessible.role: Accessible.Indicator
Accessible.name: model.isStatusOnline ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")
Accessible.name: model.desktopNotificationsAllowed ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")
}
}
Column {
id: accountLabels
spacing: 4
spacing: Style.accountLabelsSpacing
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 6
Layout.leftMargin: Style.accountLabelsLayoutMargin
anchors.top: accountAvatar.top
anchors.topMargin: Style.userStatusAnchorsMargin
anchors.left: accountAvatar.right
anchors.leftMargin: Style.accountLabelsAnchorsMargin
Label {
id: accountUser
width: 128
text: name
elide: Text.ElideRight
color: "black"
font.pixelSize: 12
font.pixelSize: Style.topLinePixelSize
font.bold: true
}
Label {
id: userStatusMessage
width: 128
text: statusMessage
elide: Text.ElideRight
color: "black"
font.pixelSize: 10
Row {
id: userStatus
visible: model.isConnected &&
model.serverHasUserStatus
anchors.top: accountUser.bottom
Label {
id: emoji
visible: model.statusEmoji !== ""
width: Style.userStatusEmojiSize
text: statusEmoji
}
Label {
id: message
anchors.bottom: emoji.bottom
anchors.left: emoji.right
anchors.leftMargin: emoji.width + Style.userStatusSpacing
visible: model.statusMessage !== ""
width: Style.currentAccountLabelWidth
text: statusMessage
elide: Text.ElideRight
color: "black"
font.pixelSize: Style.subLinePixelSize
}
}
Label {
id: accountServer
width: 128
anchors.top: userStatus.bottom
anchors.topMargin: message.visible
? message.height + Style.accountServerAnchorsMargin
: Style.userStatusAnchorsMargin
width: Style.currentAccountLabelWidth
text: server
elide: Text.ElideRight
color: "black"
font.pixelSize: 10
font.pixelSize: Style.subLinePixelSize
}
}
}

View file

@ -56,6 +56,7 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
connect(_account.data(), &AccountState::statusChanged, this, &User::statusChanged);
connect(_account.data(), &AccountState::desktopNotificationsAllowedChanged, this, &User::desktopNotificationsAllowedChanged);
connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
}
@ -214,7 +215,6 @@ void User::slotRefreshActivities()
void User::slotRefreshUserStatus()
{
// TODO: check for _account->account()->capabilities().userStatus()
if (_account.data() && _account.data()->isConnected()) {
_account.data()->fetchUserStatus();
}
@ -576,6 +576,16 @@ QUrl User::statusIcon() const
return _account->statusIcon();
}
QString User::statusEmoji() const
{
return _account->statusEmoji();
}
bool User::serverHasUserStatus() const
{
return _account->account()->capabilities().userStatus();
}
QImage User::avatar() const
{
return AvatarJob::makeCircularAvatar(_account->account()->avatar());
@ -669,7 +679,6 @@ void UserModel::buildUserList()
}
if (_init) {
_users.first()->setCurrentUser(true);
connect(_users.first(), &User::accountStateChanged, this, &UserModel::refreshCurrentUserGui);
_init = false;
}
}
@ -692,16 +701,6 @@ Q_INVOKABLE bool UserModel::isUserConnected(const int &id)
return _users[id]->isConnected();
}
Q_INVOKABLE QUrl UserModel::statusIcon(int id)
{
if (id < 0 || id >= _users.size()) {
return {};
}
return _users[id]->statusIcon();
}
QImage UserModel::avatarById(const int &id)
{
if (id < 0 || id >= _users.size())
@ -740,13 +739,21 @@ void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
connect(u, &User::statusChanged, this, [this, row] {
emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole,
UserModel::StatusEmojiRole,
UserModel::StatusMessageRole});
});
connect(u, &User::desktopNotificationsAllowedChanged, this, [this, row] {
emit dataChanged(index(row, 0), index(row, 0), { UserModel::DesktopNotificationsAllowedRole });
});
connect(u, &User::accountStateChanged, this, [this, row] {
emit dataChanged(index(row, 0), index(row, 0), { UserModel::IsConnectedRole });
});
_users << u;
if (isCurrent) {
_currentUserId = _users.indexOf(_users.last());
connect(u, &User::accountStateChanged, this, &UserModel::refreshCurrentUserGui);
}
endInsertRows();
@ -799,13 +806,10 @@ Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
{
if (_currentUserId < 0 || _currentUserId >= _users.size())
return;
disconnect(_users[_currentUserId], &User::accountStateChanged, this, &UserModel::refreshCurrentUserGui);
_users[_currentUserId]->setCurrentUser(false);
_users[id]->setCurrentUser(true);
connect(_users[id], &User::accountStateChanged, this, &UserModel::refreshCurrentUserGui);
_currentUserId = id;
emit refreshCurrentUserGui();
emit newUserSelected();
}
@ -815,7 +819,6 @@ Q_INVOKABLE void UserModel::login(const int &id)
return;
_users[id]->login();
emit refreshCurrentUserGui();
}
Q_INVOKABLE void UserModel::logout(const int &id)
@ -824,7 +827,6 @@ Q_INVOKABLE void UserModel::logout(const int &id)
return;
_users[id]->logout();
emit refreshCurrentUserGui();
}
Q_INVOKABLE void UserModel::removeAccount(const int &id)
@ -847,10 +849,6 @@ Q_INVOKABLE void UserModel::removeAccount(const int &id)
return;
}
if (_users[id]->isCurrentUser()) {
disconnect(_users[id], &User::accountStateChanged, this, &UserModel::refreshCurrentUserGui);
}
if (_users[id]->isCurrentUser() && _users.count() > 1) {
id == 0 ? switchCurrentUser(1) : switchCurrentUser(0);
}
@ -861,8 +859,6 @@ Q_INVOKABLE void UserModel::removeAccount(const int &id)
beginRemoveRows(QModelIndex(), id, id);
_users.removeAt(id);
endRemoveRows();
emit refreshCurrentUserGui();
}
int UserModel::rowCount(const QModelIndex &parent) const
@ -881,10 +877,16 @@ QVariant UserModel::data(const QModelIndex &index, int role) const
return _users[index.row()]->name();
} else if (role == ServerRole) {
return _users[index.row()]->server();
} else if (role == ServerHasUserStatusRole) {
return _users[index.row()]->serverHasUserStatus();
} else if (role == StatusIconRole) {
return _users[index.row()]->statusIcon();
} else if (role == StatusEmojiRole) {
return _users[index.row()]->statusEmoji();
} else if (role == StatusMessageRole) {
return _users[index.row()]->statusMessage();
} else if (role == DesktopNotificationsAllowedRole) {
return _users[index.row()]->isDesktopNotificationsAllowed();
} else if (role == AvatarRole) {
return _users[index.row()]->avatarUrl();
} else if (role == IsCurrentUserRole) {
@ -902,8 +904,11 @@ QHash<int, QByteArray> UserModel::roleNames() const
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[ServerRole] = "server";
roles[ServerHasUserStatusRole] = "serverHasUserStatus";
roles[StatusIconRole] = "statusIcon";
roles[StatusEmojiRole] = "statusEmoji";
roles[StatusMessageRole] = "statusMessage";
roles[DesktopNotificationsAllowedRole] = "desktopNotificationsAllowed";
roles[AvatarRole] = "avatar";
roles[IsCurrentUserRole] = "isCurrentUser";
roles[IsConnectedRole] = "isConnected";
@ -919,22 +924,6 @@ ActivityListModel *UserModel::currentActivityModel()
return _users[currentUserIndex()]->getActivityModel();
}
bool UserModel::currentUserHasActivities()
{
if (currentUserIndex() < 0 || currentUserIndex() >= _users.size())
return false;
return _users[currentUserIndex()]->hasActivities();
}
bool UserModel::currentUserHasLocalFolder()
{
if (currentUserIndex() < 0 || currentUserIndex() >= _users.size())
return false;
return _users[currentUserIndex()]->getFolder() != nullptr;
}
void UserModel::fetchCurrentActivityModel()
{
if (currentUserId() < 0 || currentUserId() >= _users.size())

View file

@ -21,11 +21,15 @@ class User : public QObject
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString server READ server CONSTANT)
Q_PROPERTY(bool serverHasUserStatus READ serverHasUserStatus CONSTANT)
Q_PROPERTY(QUrl statusIcon READ statusIcon NOTIFY statusChanged)
Q_PROPERTY(QString statusEmoji READ statusEmoji NOTIFY statusChanged)
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusChanged)
Q_PROPERTY(QString desktopNotificationsAllowed READ isDesktopNotificationsAllowed NOTIFY desktopNotificationsAllowedChanged)
Q_PROPERTY(bool hasLocalFolder READ hasLocalFolder NOTIFY hasLocalFolderChanged)
Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged)
Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
public:
User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
@ -41,6 +45,7 @@ public:
QString server(bool shortened = true) const;
bool hasLocalFolder() const;
bool serverHasTalk() const;
bool serverHasUserStatus() const;
AccountApp *talkApp() const;
bool hasActivities() const;
AccountAppList appList() const;
@ -53,6 +58,7 @@ public:
UserStatus::Status status() const;
QString statusMessage() const;
QUrl statusIcon() const;
QString statusEmoji() const;
void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
signals:
@ -63,6 +69,7 @@ signals:
void avatarChanged();
void accountStateChanged(int state);
void statusChanged();
void desktopNotificationsAllowedChanged();
public slots:
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
@ -141,11 +148,8 @@ public:
Q_INVOKABLE void openCurrentAccountServer();
Q_INVOKABLE int numUsers();
Q_INVOKABLE QString currentUserServer();
Q_INVOKABLE bool currentUserHasActivities();
Q_INVOKABLE bool currentUserHasLocalFolder();
int currentUserId() const;
Q_INVOKABLE bool isUserConnected(const int &id);
Q_INVOKABLE QUrl statusIcon(int id);
Q_INVOKABLE void switchCurrentUser(const int &id);
Q_INVOKABLE void login(const int &id);
Q_INVOKABLE void logout(const int &id);
@ -156,8 +160,11 @@ public:
enum UserRoles {
NameRole = Qt::UserRole + 1,
ServerRole,
ServerHasUserStatusRole,
StatusIconRole,
StatusEmojiRole,
StatusMessageRole,
DesktopNotificationsAllowedRole,
AvatarRole,
IsCurrentUserRole,
IsConnectedRole,
@ -168,7 +175,6 @@ public:
signals:
Q_INVOKABLE void addAccount();
Q_INVOKABLE void refreshCurrentUserGui();
Q_INVOKABLE void newUserSelected();
protected:

View file

@ -35,11 +35,6 @@ Window {
}
onVisibleChanged: {
folderStateIndicator.source = ""
folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)
? Style.stateOnlineImageSource
: Style.stateOfflineImageSource
// HACK: reload account Instantiator immediately by restting it - could be done better I guess
// see also id:accountMenu below
userLineInstantiator.active = false;
@ -48,12 +43,6 @@ Window {
Connections {
target: UserModel
onRefreshCurrentUserGui: {
folderStateIndicator.source = ""
folderStateIndicator.source = UserModel.isUserConnected(UserModel.currentUserId)
? Style.stateOnlineImageSource
: Style.stateOfflineImageSource
}
onNewUserSelected: {
accountMenu.close();
}
@ -329,6 +318,8 @@ Window {
Rectangle {
id: currentAccountStatusIndicatorBackground
visible: UserModel.currentUser.isConnected
&& UserModel.currentUser.serverHasUserStatus
width: Style.accountAvatarStateIndicatorSize + 2
height: width
anchors.bottom: currentAccountAvatar.bottom
@ -338,6 +329,9 @@ Window {
}
Rectangle {
id: currentAccountStatusIndicatorMouseHover
visible: UserModel.currentUser.isConnected
&& UserModel.currentUser.serverHasUserStatus
width: Style.accountAvatarStateIndicatorSize + 2
height: width
anchors.bottom: currentAccountAvatar.bottom
@ -349,6 +343,8 @@ Window {
Image {
id: currentAccountStatusIndicator
visible: UserModel.currentUser.isConnected
&& UserModel.currentUser.serverHasUserStatus
source: UserModel.currentUser.statusIcon
cache: false
x: currentAccountStatusIndicatorBackground.x + 1
@ -357,18 +353,19 @@ Window {
sourceSize.height: Style.accountAvatarStateIndicatorSize
Accessible.role: Accessible.Indicator
Accessible.name: UserModel.isUserStatusOnline(UserModel.currentUserId()) ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")
Accessible.name: UserModel.desktopNotificationsAllowed ? qsTr("Current user status is online") : qsTr("Current user status is do not disturb")
}
}
Column {
id: accountLabels
spacing: 4
spacing: Style.userStatusSpacing
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 6
Layout.leftMargin: Style.userStatusSpacing
anchors.top: currentAccountAvatar.top
anchors.topMargin: Style.userStatusSpacing
Label {
id: currentAccountUser
width: Style.currentAccountLabelWidth
text: UserModel.currentUser.name
elide: Text.ElideRight
@ -376,13 +373,31 @@ Window {
font.pixelSize: Style.topLinePixelSize
font.bold: true
}
Label {
Row {
id: currentUserStatus
width: Style.currentAccountLabelWidth
text: UserModel.currentUser.statusMessage
elide: Text.ElideRight
color: Style.ncTextColor
font.pixelSize: Style.subLinePixelSize
visible: UserModel.currentUser.isConnected &&
UserModel.currentUser.serverHasUserStatus
anchors.top: currentAccountUser.bottom
Label {
id: emoji
visible: UserModel.currentUser.statusEmoji !== ""
width: Style.userStatusEmojiSize
text: UserModel.currentUser.statusEmoji
}
Label {
id: message
anchors.bottom: emoji.bottom
anchors.left: emoji.right
anchors.leftMargin: emoji.width + Style.userStatusSpacing
visible: UserModel.currentUser.statusMessage !== ""
width: Style.currentAccountLabelWidth
text: UserModel.currentUser.statusMessage !== ""
? UserModel.currentUser.statusMessage
: UserModel.currentUser.server
elide: Text.ElideRight
color: Style.ncTextColor
font.pixelSize: Style.subLinePixelSize
}
}
}
@ -429,7 +444,8 @@ Window {
Image {
id: folderStateIndicator
source: UserModel.isUserConnected(UserModel.currentUserId)
visible: UserModel.currentUser.hasLocalFolder
source: UserModel.currentUser.isConnected
? Style.stateOnlineImageSource
: Style.stateOfflineImageSource
cache: false
@ -440,7 +456,7 @@ Window {
sourceSize.height: Style.folderStateIndicatorSize
Accessible.role: Accessible.Indicator
Accessible.name: UserModel.isUserConnected(UserModel.currentUserId()) ? qsTr("Connected") : qsTr("Disconnected")
Accessible.name: UserModel.currentUser.isConnected ? qsTr("Connected") : qsTr("Disconnected")
}
Accessible.role: Accessible.Button

View file

@ -19,6 +19,7 @@
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include "theme.h"
#include "capabilities.h"
#include <QTimer>
#include <QJsonDocument>
@ -28,43 +29,52 @@ namespace OCC {
Q_LOGGING_CATEGORY(lcUserStatus, "nextcloud.gui.userstatus", QtInfoMsg)
namespace {
UserStatus::Status stringToEnum(const QString &status)
{
// it needs to match the Status enum
const QHash<QString, UserStatus::Status> preDefinedStatus{
{"online", UserStatus::Status::Online},
{"dnd", UserStatus::Status::DoNotDisturb},
{"away", UserStatus::Status::Away},
{"offline", UserStatus::Status::Offline},
{"invisible", UserStatus::Status::Invisible}
};
// api should return invisible, dnd,... toLower() it is to make sure
// it matches _preDefinedStatus, otherwise the default is online (0)
return preDefinedStatus.value(status.toLower(), UserStatus::Status::Online);
}
QString enumToString(UserStatus::Status status)
{
switch (status) {
case UserStatus::Status::Away:
return QObject::tr("Away");
case UserStatus::Status::DoNotDisturb:
return QObject::tr("Do not disturb");
case UserStatus::Status::Invisible:
case UserStatus::Status::Offline:
return QObject::tr("Offline");
case UserStatus::Status::Online:
return QObject::tr("Online");
}
Q_UNREACHABLE();
}
}
UserStatus::UserStatus(QObject *parent)
: QObject(parent)
{
}
UserStatus::Status UserStatus::stringToEnum(const QString &status) const
{
// it needs to match the Status enum
const QHash<QString, Status> preDefinedStatus{{"online", Status::Online},
{"dnd", Status::DoNotDisturb}, //DoNotDisturb
{"away", Status::Away},
{"offline", Status::Offline},
{"invisible", Status::Invisible}};
// api should return invisible, dnd,... toLower() it is to make sure
// it matches _preDefinedStatus, otherwise the default is online (0)
const auto statusKey = status.isEmpty() ? QStringLiteral("online") : status.toLower();
return preDefinedStatus.value(statusKey, Status::Online);
}
QString UserStatus::enumToString(Status status) const
{
switch (status) {
case Status::Away:
return tr("Away");
case Status::DoNotDisturb:
return tr("Do not disturb");
case Status::Invisible:
case Status::Offline:
return tr("Offline");
default:
return tr("Online");
}
}
void UserStatus::fetchUserStatus(AccountPtr account)
{
if (!account->capabilities().userStatus()) {
return;
}
if (_job) {
_job->deleteLater();
}
@ -79,7 +89,9 @@ void UserStatus::slotFetchUserStatusFinished(const QJsonDocument &json, int stat
const QJsonObject defaultValues {
{"icon", ""},
{"message", ""},
{"status", "online"}
{"status", "online"},
{"messageIsPredefined", "false"},
{"statusIsUserDefined", "false"}
};
if (statusCode != 200) {
@ -88,13 +100,11 @@ void UserStatus::slotFetchUserStatusFinished(const QJsonDocument &json, int stat
}
const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject(defaultValues);
const auto emoji = retrievedData.value("icon").toString();
const auto message = retrievedData.value("message").toString();
_status = stringToEnum(retrievedData.value("status").toString());
const auto visibleStatusText = message.isEmpty() ? enumToString(_status) : message;
_message = QString("%1 %2").arg(emoji, visibleStatusText);
_emoji = retrievedData.value("icon").toString().trimmed();
_status = stringToEnum(retrievedData.value("status").toString());
_message = retrievedData.value("message").toString().trimmed();
emit fetchUserStatusFinished();
}
@ -105,7 +115,12 @@ UserStatus::Status UserStatus::status() const
QString UserStatus::message() const
{
return _message.trimmed();
return _message;
}
QString UserStatus::emoji() const
{
return _emoji;
}
QUrl UserStatus::icon() const
@ -118,9 +133,11 @@ QUrl UserStatus::icon() const
case Status::Invisible:
case Status::Offline:
return Theme::instance()->statusInvisibleImageSource();
default:
case Status::Online:
return Theme::instance()->statusOnlineImageSource();
}
Q_UNREACHABLE();
}
} // namespace OCC

View file

@ -39,6 +39,7 @@ public:
void fetchUserStatus(AccountPtr account);
Status status() const;
QString message() const;
QString emoji() const;
QUrl icon() const;
private slots:
@ -48,11 +49,10 @@ signals:
void fetchUserStatusFinished();
private:
Status stringToEnum(const QString &status) const;
QString enumToString(Status status) const;
QPointer<JsonApiJob> _job; // the currently running job
Status _status = Status::Online;
QString _message;
QString _emoji;
};

View file

@ -189,7 +189,9 @@ bool Capabilities::chunkingNg() const
bool Capabilities::userStatus() const
{
return _capabilities.contains("notifications") && _capabilities["notifications"].toMap().contains("user-status");
return _capabilities.contains("notifications") &&
_capabilities["notifications"].toMap().contains("ocs-endpoints") &&
_capabilities["notifications"].toMap()["ocs-endpoints"].toStringList().contains("user-status");
}
PushNotificationTypes Capabilities::availablePushNotifications() const

View file

@ -44,6 +44,14 @@ QtObject {
property int headerButtonIconSize: 32
property int activityLabelBaseWidth: 240
property int userStatusEmojiSize: 8
property int userStatusSpacing: 6
property int userStatusAnchorsMargin: 2
property int accountServerAnchorsMargin: 10
property int accountLabelsSpacing: 4
property int accountLabelsAnchorsMargin: 7
property int accountLabelsLayoutMargin: 9
// Visual behaviour
property bool hoverEffectsEnabled: true