Add dialog to set user status

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
This commit is contained in:
Felix Weilbach 2021-09-09 11:18:22 +02:00
parent f34d663029
commit 8a8d488454
42 changed files with 4592 additions and 294 deletions

View File

@ -1,5 +1,9 @@
<RCC>
<qresource prefix="/qml">
<file>src/gui/UserStatusSelector.qml</file>
<file>src/gui/UserStatusSelectorDialog.qml</file>
<file>src/gui/EmojiPicker.qml</file>
<file>src/gui/ErrorBox.qml</file>
<file>src/gui/tray/Window.qml</file>
<file>src/gui/tray/UserLine.qml</file>
<file>src/gui/tray/HeaderButton.qml</file>

View File

@ -35,6 +35,8 @@ set(client_UI_SRCS
addcertificatedialog.ui
proxyauthdialog.ui
mnemonicdialog.ui
UserStatusSelector.qml
UserStatusSelectorDialog.qml
tray/ActivityActionButton.qml
tray/ActivityItem.qml
tray/Window.qml
@ -92,7 +94,6 @@ set(client_SRCS
systray.cpp
thumbnailjob.cpp
userinfo.cpp
userstatus.cpp
accountstate.cpp
addcertificatedialog.cpp
authenticationdialog.cpp
@ -106,6 +107,8 @@ set(client_SRCS
iconjob.cpp
iconutils.cpp
remotewipe.cpp
userstatusselectormodel.cpp
emojimodel.cpp
tray/ActivityData.cpp
tray/ActivityListModel.cpp
tray/UserModel.cpp

112
src/gui/EmojiPicker.qml Normal file
View File

@ -0,0 +1,112 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import com.nextcloud.desktopclient 1.0 as NC
ColumnLayout {
NC.EmojiModel {
id: emojiModel
}
signal chosen(string emoji)
spacing: 0
FontMetrics {
id: metrics
}
ListView {
id: headerLayout
Layout.fillWidth: true
implicitWidth: contentItem.childrenRect.width
implicitHeight: metrics.height * 2
orientation: ListView.Horizontal
model: emojiModel.emojiCategoriesModel
delegate: ItemDelegate {
width: metrics.height * 2
height: headerLayout.height
contentItem: Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: emoji
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 2
visible: ListView.isCurrentItem
color: "grey"
}
onClicked: {
emojiModel.setCategory(label)
}
}
}
Rectangle {
height: 1
Layout.fillWidth: true
color: "grey"
}
GridView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: metrics.height * 8
cellWidth: metrics.height * 2
cellHeight: metrics.height * 2
boundsBehavior: Flickable.DragOverBounds
clip: true
model: emojiModel.model
delegate: ItemDelegate {
width: metrics.height * 2
height: metrics.height * 2
contentItem: Text {
anchors.centerIn: parent
text: modelData === undefined ? "" : modelData.unicode
}
onClicked: {
chosen(modelData.unicode);
emojiModel.emojiUsed(modelData);
}
}
ScrollBar.vertical: ScrollBar {}
}
}

26
src/gui/ErrorBox.qml Normal file
View File

@ -0,0 +1,26 @@
import QtQuick 2.15
Item {
id: errorBox
property var text: ""
implicitHeight: errorMessage.implicitHeight + 2 * 8
Rectangle {
anchors.fill: parent
color: "red"
border.color: "black"
}
Text {
id: errorMessage
anchors.fill: parent
anchors.margins: 8
width: parent.width
color: "white"
wrapMode: Text.WordWrap
text: errorBox.text
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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.
*/
import QtQuick 2.6
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import com.nextcloud.desktopclient 1.0 as NC
ColumnLayout {
id: rootLayout
spacing: 0
property NC.UserStatusSelectorModel userStatusSelectorModel
FontMetrics {
id: metrics
}
Text {
Layout.topMargin: 16
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
font.bold: true
text: qsTr("Online status")
}
GridLayout {
Layout.margins: 8
Layout.alignment: Qt.AlignTop
columns: 2
rows: 2
columnSpacing: 8
rowSpacing: 8
Button {
Layout.fillWidth: true
checked: NC.UserStatus.Online == userStatusSelectorModel.onlineStatus
checkable: true
icon.source: userStatusSelectorModel.onlineIcon
icon.color: "transparent"
text: qsTr("Online")
onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Online)
implicitWidth: 100
}
Button {
Layout.fillWidth: true
checked: NC.UserStatus.Away == userStatusSelectorModel.onlineStatus
checkable: true
icon.source: userStatusSelectorModel.awayIcon
icon.color: "transparent"
text: qsTr("Away")
onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Away)
implicitWidth: 100
}
Button {
Layout.fillWidth: true
checked: NC.UserStatus.DoNotDisturb == userStatusSelectorModel.onlineStatus
checkable: true
icon.source: userStatusSelectorModel.dndIcon
icon.color: "transparent"
text: qsTr("Do not disturb")
onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.DoNotDisturb)
implicitWidth: 100
}
Button {
Layout.fillWidth: true
checked: NC.UserStatus.Invisible == userStatusSelectorModel.onlineStatus
checkable: true
icon.source: userStatusSelectorModel.invisibleIcon
icon.color: "transparent"
text: qsTr("Invisible")
onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Invisible)
implicitWidth: 100
}
}
Text {
Layout.topMargin: 16
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
font.bold: true
text: qsTr("Status message")
}
RowLayout {
Layout.topMargin: 8
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 16
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Button {
Layout.preferredWidth: userStatusMessageTextField.height // metrics.height * 2
Layout.preferredHeight: userStatusMessageTextField.height // metrics.height * 2
text: userStatusSelectorModel.userStatusEmoji
onClicked: emojiDialog.open()
}
Popup {
id: emojiDialog
padding: 0
margins: 0
anchors.centerIn: Overlay.overlay
EmojiPicker {
id: emojiPicker
onChosen: {
userStatusSelectorModel.userStatusEmoji = emoji
emojiDialog.close()
}
}
}
TextField {
id: userStatusMessageTextField
Layout.fillWidth: true
placeholderText: qsTr("What is your Status?")
text: userStatusSelectorModel.userStatusMessage
onEditingFinished: userStatusSelectorModel.setUserStatusMessage(text)
}
}
Repeater {
model: userStatusSelectorModel.predefinedStatusesCount
Button {
id: control
Layout.fillWidth: true
flat: !hovered
hoverEnabled: true
text: userStatusSelectorModel.predefinedStatus(index).icon + " <b>" + userStatusSelectorModel.predefinedStatus(index).message + "</b> - " + userStatusSelectorModel.predefinedStatusClearAt(index)
onClicked: userStatusSelectorModel.setPredefinedStatus(index)
}
}
RowLayout {
Layout.topMargin: 16
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.bottomMargin: 8
Layout.alignment: Qt.AlignTop
Text {
text: qsTr("Clear status message after")
}
ComboBox {
Layout.fillWidth: true
model: userStatusSelectorModel.clearAtValues
displayText: userStatusSelectorModel.clearAt
onActivated: userStatusSelectorModel.setClearAt(index)
}
}
RowLayout {
Layout.margins: 8
Layout.alignment: Qt.AlignTop
Button {
Layout.fillWidth: true
text: qsTr("Clear status message")
onClicked: userStatusSelectorModel.clearUserStatus()
}
Button {
highlighted: true
Layout.fillWidth: true
text: qsTr("Set status message")
onClicked: userStatusSelectorModel.setUserStatus()
}
}
ErrorBox {
Layout.margins: 8
Layout.fillWidth: true
visible: userStatusSelectorModel.errorMessage != ""
text: "<b>Error:</b> " + userStatusSelectorModel.errorMessage
}
}

View File

@ -0,0 +1,29 @@
import QtQuick.Window 2.15
import com.nextcloud.desktopclient 1.0 as NC
Window {
id: dialog
property NC.UserStatusSelectorModel model: NC.UserStatusSelectorModel {
onFinished: {
dialog.close()
}
}
width: view.implicitWidth
height: view.implicitHeight
minimumWidth: view.implicitWidth
minimumHeight: view.implicitHeight
maximumWidth: view.implicitWidth
maximumHeight: view.implicitHeight
visible: true
flags: Qt.Dialog
UserStatusSelector {
id: view
userStatusSelectorModel: model
}
}

View File

@ -44,7 +44,6 @@ AccountState::AccountState(AccountPtr account)
, _waitingForNewCredentials(false)
, _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
, _remoteWipe(new RemoteWipe(_account))
, _userStatus(new UserStatus(this))
, _isDesktopNotificationsAllowed(true)
{
qRegisterMetaType<AccountState *>("AccountState*");
@ -127,26 +126,6 @@ void AccountState::setState(State state)
emit stateChanged(_state);
}
UserStatus::Status AccountState::status() const
{
return _userStatus->status();
}
QString AccountState::statusMessage() const
{
return _userStatus->message();
}
QUrl AccountState::statusIcon() const
{
return _userStatus->icon();
}
QString AccountState::statusEmoji() const
{
return _userStatus->emoji();
}
QString AccountState::stateString(State state)
{
switch (state) {
@ -462,12 +441,6 @@ void AccountState::fetchNavigationApps(){
job->getNavigationApps();
}
void AccountState::fetchUserStatus()
{
connect(_userStatus, &UserStatus::fetchUserStatusFinished, this, &AccountState::statusChanged);
_userStatus->fetchUserStatus(_account);
}
void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
if(statusCode == 200){
qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value;

View File

@ -21,7 +21,7 @@
#include <QPointer>
#include "connectionvalidator.h"
#include "creds/abstractcredentials.h"
#include "userstatus.h"
#include <memory>
class QSettings;
@ -162,23 +162,6 @@ public:
///Asks for user credentials
void handleInvalidCredentials();
/** Returns the user status (Online, Dnd, Away, Offline, Invisible)
* https://gist.github.com/georgehrke/55a0412007f13be1551d1f9436a39675
*/
UserStatus::Status status() const;
/** 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
*/
@ -188,10 +171,6 @@ public:
*/
void setDesktopNotificationsAllowed(bool isAllowed);
/** Fetch the user status (status, icon, message)
*/
void fetchUserStatus();
public slots:
/// Triggers a ping to the server to update state and
/// connection status and errors.
@ -256,7 +235,6 @@ private:
*/
AccountAppList _apps;
UserStatus *_userStatus;
bool _isDesktopNotificationsAllowed;
};

1571
src/gui/emojimodel.cpp Normal file

File diff suppressed because it is too large Load Diff

137
src/gui/emojimodel.h Normal file
View File

@ -0,0 +1,137 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 <QObject>
#include <QSettings>
#include <QObject>
#include <QQmlEngine>
#include <QVariant>
#include <QVector>
#include <QAbstractItemModel>
namespace OCC {
struct Emoji
{
Emoji(QString u, QString s, bool isCustom = false)
: unicode(std::move(std::move(u)))
, shortname(std::move(std::move(s)))
, isCustom(isCustom)
{
}
Emoji() = default;
friend QDataStream &operator<<(QDataStream &arch, const Emoji &object)
{
arch << object.unicode;
arch << object.shortname;
return arch;
}
friend QDataStream &operator>>(QDataStream &arch, Emoji &object)
{
arch >> object.unicode;
arch >> object.shortname;
object.isCustom = object.unicode.startsWith("image://");
return arch;
}
QString unicode;
QString shortname;
bool isCustom = false;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER unicode)
Q_PROPERTY(QString shortname MEMBER shortname)
Q_PROPERTY(bool isCustom MEMBER isCustom)
};
class EmojiCategoriesModel : public QAbstractListModel
{
public:
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
private:
enum Roles {
EmojiRole = 0,
LabelRole
};
struct Category
{
QString emoji;
QString label;
};
static const std::vector<Category> categories;
};
class EmojiModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantList model READ model NOTIFY modelChanged)
Q_PROPERTY(QAbstractListModel *emojiCategoriesModel READ emojiCategoriesModel CONSTANT)
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
Q_PROPERTY(QVariantList people MEMBER people CONSTANT)
Q_PROPERTY(QVariantList nature MEMBER nature CONSTANT)
Q_PROPERTY(QVariantList food MEMBER food CONSTANT)
Q_PROPERTY(QVariantList activity MEMBER activity CONSTANT)
Q_PROPERTY(QVariantList travel MEMBER travel CONSTANT)
Q_PROPERTY(QVariantList objects MEMBER objects CONSTANT)
Q_PROPERTY(QVariantList symbols MEMBER symbols CONSTANT)
Q_PROPERTY(QVariantList flags MEMBER flags CONSTANT)
public:
explicit EmojiModel(QObject *parent = nullptr)
: QObject(parent)
{
}
Q_INVOKABLE QVariantList history() const;
Q_INVOKABLE void setCategory(const QString &category);
Q_INVOKABLE void emojiUsed(const QVariant &modelData);
QVariantList model() const;
QAbstractListModel *emojiCategoriesModel();
signals:
void historyChanged();
void modelChanged();
private:
static const QVariantList people;
static const QVariantList nature;
static const QVariantList food;
static const QVariantList activity;
static const QVariantList travel;
static const QVariantList objects;
static const QVariantList symbols;
static const QVariantList flags;
QSettings _settings;
QString _category = "history";
EmojiCategoriesModel _emojiCategoriesModel;
};
}
Q_DECLARE_METATYPE(OCC::Emoji)

View File

@ -16,6 +16,7 @@
#include <cmath>
#include <csignal>
#include <qqml.h>
#ifdef Q_OS_UNIX
#include <sys/time.h>
@ -26,6 +27,8 @@
#include "theme.h"
#include "common/utility.h"
#include "cocoainitializer.h"
#include "userstatusselectormodel.h"
#include "emojimodel.h"
#if defined(BUILD_UPDATER)
#include "updater/updater.h"
@ -54,6 +57,15 @@ int main(int argc, char **argv)
Q_INIT_RESOURCE(resources);
Q_INIT_RESOURCE(theme);
qmlRegisterType<EmojiModel>("com.nextcloud.desktopclient", 1, 0, "EmojiModel");
qRegisterMetaTypeStreamOperators<Emoji>();
qmlRegisterType<UserStatusSelectorModel>("com.nextcloud.desktopclient", 1, 0,
"UserStatusSelectorModel");
qmlRegisterUncreatableType<OCC::UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus",
"Access to Status enum");
qRegisterMetaType<OCC::UserStatus>("UserStatus");
// Work around a bug in KDE's qqc2-desktop-style which breaks
// buttons with icons not based on a name, by forcing a style name
// the platformtheme plugin won't try to force qqc2-desktops-style

View File

@ -617,7 +617,7 @@ void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener)
params.addQueryItem("path", fileData.serverRelativePath);
params.addQueryItem("editorId", editor->id());
job->addQueryParams(params);
job->usePOST();
job->setVerb(JsonApiJob::Verb::Post);
QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){
auto data = json.object().value("ocs").toObject().value("data").toObject();

View File

@ -14,6 +14,11 @@ MenuItem {
Accessible.role: Accessible.MenuItem
Accessible.name: qsTr("Account entry")
property variant dialog;
property variant comp;
signal showUserStatusSelectorDialog(int id)
RowLayout {
id: userLineLayout
spacing: 0
@ -188,6 +193,29 @@ MenuItem {
radius: 2
}
MenuItem {
visible: model.isConnected && model.serverHasUserStatus
height: visible ? implicitHeight : 0
text: qsTr("Set status")
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: {
showUserStatusSelectorDialog(index)
accountMenu.close()
}
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
}
}
}
MenuItem {
text: model.isConnected ? qsTr("Log out") : qsTr("Log in")
font.pixelSize: Style.topLinePixelSize

View File

@ -4,6 +4,7 @@
#include "accountmanager.h"
#include "owncloudgui.h"
#include <pushnotifications.h>
#include "userstatusselectormodel.h"
#include "syncengine.h"
#include "ocsjob.h"
#include "configfile.h"
@ -11,7 +12,9 @@
#include "logger.h"
#include "guiutility.h"
#include "syncfileitem.h"
#include "tray/ActivityListModel.h"
#include "tray/NotificationCache.h"
#include "userstatusconnector.h"
#include <QDesktopServices>
#include <QIcon>
@ -25,8 +28,8 @@
#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
namespace {
constexpr qint64 expiredActivitiesCheckIntervalMsecs = 1000 * 60;
constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
constexpr qint64 expiredActivitiesCheckIntervalMsecs = 1000 * 60;
constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
}
namespace OCC {
@ -65,7 +68,7 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
connect(_account.data(), &AccountState::statusChanged, this, &User::statusChanged);
connect(_account->account().data(), &Account::userStatusChanged, this, &User::statusChanged);
connect(_account.data(), &AccountState::desktopNotificationsAllowedChanged, this, &User::desktopNotificationsAllowedChanged);
connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
@ -236,10 +239,10 @@ void User::slotRefreshActivities()
_activityModel->slotRefreshActivity();
}
void User::slotRefreshUserStatus()
void User::slotRefreshUserStatus()
{
if (_account.data() && _account.data()->isConnected()) {
_account.data()->fetchUserStatus();
_account->account()->userStatusConnector()->fetchUserStatus();
}
}
@ -621,29 +624,29 @@ QString User::server(bool shortened) const
return serverUrl;
}
UserStatus::Status User::status() const
UserStatus::OnlineStatus User::status() const
{
return _account->status();
return _account->account()->userStatusConnector()->userStatus().state();
}
QString User::statusMessage() const
{
return _account->statusMessage();
return _account->account()->userStatusConnector()->userStatus().message();
}
QUrl User::statusIcon() const
{
return _account->statusIcon();
return _account->account()->userStatusConnector()->userStatus().stateIcon();
}
QString User::statusEmoji() const
{
return _account->statusEmoji();
return _account->account()->userStatusConnector()->userStatus().icon();
}
bool User::serverHasUserStatus() const
{
return _account->account()->capabilities().userStatus();
return _account->account()->capabilities().userStatusNotification();
}
QImage User::avatar() const
@ -921,6 +924,15 @@ Q_INVOKABLE void UserModel::removeAccount(const int &id)
endRemoveRows();
}
std::shared_ptr<OCC::UserStatusConnector> UserModel::userStatusConnector(int id)
{
if (id < 0 || id >= _users.size()) {
return nullptr;
}
return _users[id]->account()->userStatusConnector();
}
int UserModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);

View File

@ -12,6 +12,8 @@
#include "accountmanager.h"
#include "folderman.h"
#include "NotificationCache.h"
#include "userstatusselectormodel.h"
#include "userstatusconnector.h"
#include <chrono>
namespace OCC {
@ -55,7 +57,7 @@ public:
void removeAccount() const;
QString avatarUrl() const;
bool isDesktopNotificationsAllowed() const;
UserStatus::Status status() const;
UserStatus::OnlineStatus status() const;
QString statusMessage() const;
QUrl statusIcon() const;
QString statusEmoji() const;
@ -158,6 +160,8 @@ public:
Q_INVOKABLE void logout(const int &id);
Q_INVOKABLE void removeAccount(const int &id);
Q_INVOKABLE std::shared_ptr<OCC::UserStatusConnector> userStatusConnector(int id);
ActivityListModel *currentActivityModel();
enum UserRoles {

View File

@ -140,6 +140,10 @@ Window {
}
}
Loader {
id: userStatusSelectorDialogLoader
}
Menu {
id: accountMenu
@ -167,7 +171,14 @@ Window {
Instantiator {
id: userLineInstantiator
model: UserModel
delegate: UserLine {}
delegate: UserLine {
onShowUserStatusSelectorDialog: {
userStatusSelectorDialogLoader.source = "qrc:/qml/src/gui/UserStatusSelectorDialog.qml"
userStatusSelectorDialogLoader.item.title = qsTr("Set user status")
userStatusSelectorDialogLoader.item.model.load(index)
userStatusSelectorDialogLoader.item.show()
}
}
onObjectAdded: accountMenu.insertItem(index, object)
onObjectRemoved: accountMenu.removeItem(object)
}

View File

@ -1,126 +0,0 @@
/*
* Copyright (C) by Camila <hello@camila.codes>
*
* 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 "userstatus.h"
#include "account.h"
#include "accountstate.h"
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include "theme.h"
#include "capabilities.h"
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
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);
}
}
UserStatus::UserStatus(QObject *parent)
: QObject(parent)
{
}
void UserStatus::fetchUserStatus(AccountPtr account)
{
if (!account->capabilities().userStatus()) {
return;
}
if (_job) {
_job->deleteLater();
}
_job = new JsonApiJob(account, QStringLiteral("/ocs/v2.php/apps/user_status/api/v1/user_status"), this);
connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserStatus::slotFetchUserStatusFinished);
_job->start();
}
void UserStatus::slotFetchUserStatusFinished(const QJsonDocument &json, int statusCode)
{
const QJsonObject defaultValues {
{"icon", ""},
{"message", ""},
{"status", "online"},
{"messageIsPredefined", "false"},
{"statusIsUserDefined", "false"}
};
if (statusCode != 200) {
qCInfo(lcUserStatus) << "Slot fetch UserStatus finished with status code" << statusCode;
qCInfo(lcUserStatus) << "Using then default values as if user has not set any status" << defaultValues;
}
const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject(defaultValues);
_emoji = retrievedData.value("icon").toString().trimmed();
_status = stringToEnum(retrievedData.value("status").toString());
_message = retrievedData.value("message").toString().trimmed();
emit fetchUserStatusFinished();
}
UserStatus::Status UserStatus::status() const
{
return _status;
}
QString UserStatus::message() const
{
return _message;
}
QString UserStatus::emoji() const
{
return _emoji;
}
QUrl UserStatus::icon() const
{
switch (_status) {
case Status::Away:
return Theme::instance()->statusAwayImageSource();
case Status::DoNotDisturb:
return Theme::instance()->statusDoNotDisturbImageSource();
case Status::Invisible:
case Status::Offline:
return Theme::instance()->statusInvisibleImageSource();
case Status::Online:
return Theme::instance()->statusOnlineImageSource();
}
Q_UNREACHABLE();
}
} // namespace OCC

View File

@ -1,61 +0,0 @@
/*
* Copyright (C) by Camila <hello@camila.codes>
*
* 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 USERSTATUS_H
#define USERSTATUS_H
#include <QPointer>
#include "accountfwd.h"
namespace OCC {
class JsonApiJob;
class UserStatus : public QObject
{
Q_OBJECT
public:
explicit UserStatus(QObject *parent = nullptr);
enum class Status {
Online,
DoNotDisturb,
Away,
Offline,
Invisible
};
Q_ENUM(Status);
void fetchUserStatus(AccountPtr account);
Status status() const;
QString message() const;
QString emoji() const;
QUrl icon() const;
private slots:
void slotFetchUserStatusFinished(const QJsonDocument &json, int statusCode);
signals:
void fetchUserStatusFinished();
private:
QPointer<JsonApiJob> _job; // the currently running job
Status _status = Status::Online;
QString _message;
QString _emoji;
};
} // namespace OCC
#endif //USERSTATUS_H

View File

@ -0,0 +1,474 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "userstatusselectormodel.h"
#include "tray/UserModel.h"
#include <ocsuserstatusconnector.h>
#include <qnamespace.h>
#include <userstatusconnector.h>
#include <theme.h>
#include <QDateTime>
#include <QLoggingCategory>
#include <algorithm>
#include <cmath>
#include <cstddef>
namespace OCC {
Q_LOGGING_CATEGORY(lcUserStatusDialogModel, "nextcloud.gui.userstatusdialogmodel", QtInfoMsg)
UserStatusSelectorModel::UserStatusSelectorModel(QObject *parent)
: QObject(parent)
, _dateTimeProvider(new DateTimeProvider)
{
_userStatus.setIcon("😀");
}
UserStatusSelectorModel::UserStatusSelectorModel(std::shared_ptr<UserStatusConnector> userStatusConnector, QObject *parent)
: QObject(parent)
, _userStatusConnector(userStatusConnector)
, _userStatus("no-id", "", "😀", UserStatus::OnlineStatus::Online, false, {})
, _dateTimeProvider(new DateTimeProvider)
{
_userStatus.setIcon("😀");
init();
}
UserStatusSelectorModel::UserStatusSelectorModel(std::shared_ptr<UserStatusConnector> userStatusConnector,
std::unique_ptr<DateTimeProvider> dateTimeProvider,
QObject *parent)
: QObject(parent)
, _userStatusConnector(userStatusConnector)
, _dateTimeProvider(std::move(dateTimeProvider))
{
_userStatus.setIcon("😀");
init();
}
UserStatusSelectorModel::UserStatusSelectorModel(const UserStatus &userStatus,
std::unique_ptr<DateTimeProvider> dateTimeProvider, QObject *parent)
: QObject(parent)
, _userStatus(userStatus)
, _dateTimeProvider(std::move(dateTimeProvider))
{
_userStatus.setIcon("😀");
}
UserStatusSelectorModel::UserStatusSelectorModel(const UserStatus &userStatus,
QObject *parent)
: QObject(parent)
, _userStatus(userStatus)
{
_userStatus.setIcon("😀");
}
void UserStatusSelectorModel::load(int id)
{
reset();
_userStatusConnector = UserModel::instance()->userStatusConnector(id);
init();
}
void UserStatusSelectorModel::reset()
{
if (_userStatusConnector) {
disconnect(_userStatusConnector.get(), &UserStatusConnector::userStatusFetched, this,
&UserStatusSelectorModel::onUserStatusFetched);
disconnect(_userStatusConnector.get(), &UserStatusConnector::predefinedStatusesFetched, this,
&UserStatusSelectorModel::onPredefinedStatusesFetched);
disconnect(_userStatusConnector.get(), &UserStatusConnector::error, this,
&UserStatusSelectorModel::onError);
disconnect(_userStatusConnector.get(), &UserStatusConnector::userStatusSet, this,
&UserStatusSelectorModel::onUserStatusSet);
disconnect(_userStatusConnector.get(), &UserStatusConnector::messageCleared, this,
&UserStatusSelectorModel::onMessageCleared);
}
_userStatusConnector = nullptr;
}
void UserStatusSelectorModel::init()
{
if (!_userStatusConnector) {
return;
}
connect(_userStatusConnector.get(), &UserStatusConnector::userStatusFetched, this,
&UserStatusSelectorModel::onUserStatusFetched);
connect(_userStatusConnector.get(), &UserStatusConnector::predefinedStatusesFetched, this,
&UserStatusSelectorModel::onPredefinedStatusesFetched);
connect(_userStatusConnector.get(), &UserStatusConnector::error, this,
&UserStatusSelectorModel::onError);
connect(_userStatusConnector.get(), &UserStatusConnector::userStatusSet, this,
&UserStatusSelectorModel::onUserStatusSet);
connect(_userStatusConnector.get(), &UserStatusConnector::messageCleared, this,
&UserStatusSelectorModel::onMessageCleared);
_userStatusConnector->fetchUserStatus();
_userStatusConnector->fetchPredefinedStatuses();
}
UserStatusSelectorModel::~UserStatusSelectorModel()
{
qCDebug(lcUserStatusDialogModel) << "Destroyed";
}
void UserStatusSelectorModel::onUserStatusSet()
{
qCDebug(lcUserStatusDialogModel) << "Emit finished";
emit finished();
}
void UserStatusSelectorModel::onMessageCleared()
{
emit finished();
}
void UserStatusSelectorModel::onError(UserStatusConnector::Error error)
{
qCWarning(lcUserStatusDialogModel) << "Error:" << error;
switch (error) {
case UserStatusConnector::Error::CouldNotFetchPredefinedUserStatuses:
setError(tr("Could not fetch predefined statuses. Make sure you are connected to the server."));
return;
case UserStatusConnector::Error::CouldNotFetchUserStatus:
setError(tr("Could not fetch user status. Make sure you are connected to the server."));
return;
case UserStatusConnector::Error::UserStatusNotSupported:
setError(tr("User status feature is not supported. You will not be able to set your user status."));
return;
case UserStatusConnector::Error::EmojisNotSupported:
setError(tr("Emojis feature is not supported. Some user status functionality may not work."));
return;
case UserStatusConnector::Error::CouldNotSetUserStatus:
setError(tr("Could not set user status. Make sure you are connected to the server."));
return;
case UserStatusConnector::Error::CouldNotClearMessage:
setError(tr("Could not clear user status message. Make sure you are connected to the server."));
return;
}
Q_UNREACHABLE();
}
void UserStatusSelectorModel::setError(const QString &reason)
{
_errorMessage = reason;
emit errorMessageChanged();
}
void UserStatusSelectorModel::clearError()
{
setError("");
}
void UserStatusSelectorModel::setOnlineStatus(UserStatus::OnlineStatus status)
{
if (status == _userStatus.state()) {
return;
}
_userStatus.setState(status);
emit onlineStatusChanged();
}
QUrl UserStatusSelectorModel::onlineIcon() const
{
return Theme::instance()->statusOnlineImageSource();
}
QUrl UserStatusSelectorModel::awayIcon() const
{
return Theme::instance()->statusAwayImageSource();
}
QUrl UserStatusSelectorModel::dndIcon() const
{
return Theme::instance()->statusDoNotDisturbImageSource();
}
QUrl UserStatusSelectorModel::invisibleIcon() const
{
return Theme::instance()->statusInvisibleImageSource();
}
UserStatus::OnlineStatus UserStatusSelectorModel::onlineStatus() const
{
return _userStatus.state();
}
QString UserStatusSelectorModel::userStatusMessage() const
{
return _userStatus.message();
}
void UserStatusSelectorModel::setUserStatusMessage(const QString &message)
{
_userStatus.setMessage(message);
_userStatus.setMessagePredefined(false);
emit userStatusChanged();
}
void UserStatusSelectorModel::setUserStatusEmoji(const QString &emoji)
{
_userStatus.setIcon(emoji);
_userStatus.setMessagePredefined(false);
emit userStatusChanged();
}
QString UserStatusSelectorModel::userStatusEmoji() const
{
return _userStatus.icon();
}
void UserStatusSelectorModel::onUserStatusFetched(const UserStatus &userStatus)
{
if (userStatus.state() != UserStatus::OnlineStatus::Offline) {
_userStatus.setState(userStatus.state());
}
_userStatus.setMessage(userStatus.message());
_userStatus.setMessagePredefined(userStatus.messagePredefined());
_userStatus.setId(userStatus.id());
_userStatus.setClearAt(userStatus.clearAt());
if (!userStatus.icon().isEmpty()) {
_userStatus.setIcon(userStatus.icon());
}
emit userStatusChanged();
emit onlineStatusChanged();
emit clearAtChanged();
}
Optional<ClearAt> UserStatusSelectorModel::clearStageTypeToDateTime(ClearStageType type) const
{
switch (type) {
case ClearStageType::DontClear:
return {};
case ClearStageType::HalfHour: {
ClearAt clearAt;
clearAt._type = ClearAtType::Period;
clearAt._period = 60 * 30;
return clearAt;
}
case ClearStageType::OneHour: {
ClearAt clearAt;
clearAt._type = ClearAtType::Period;
clearAt._period = 60 * 60;
return clearAt;
}
case ClearStageType::FourHour: {
ClearAt clearAt;
clearAt._type = ClearAtType::Period;
clearAt._period = 60 * 60 * 4;
return clearAt;
}
case ClearStageType::Today: {
ClearAt clearAt;
clearAt._type = ClearAtType::EndOf;
clearAt._endof = "day";
return clearAt;
}
case ClearStageType::Week: {
ClearAt clearAt;
clearAt._type = ClearAtType::EndOf;
clearAt._endof = "week";
return clearAt;
}
default:
Q_UNREACHABLE();
}
}
void UserStatusSelectorModel::setUserStatus()
{
Q_ASSERT(_userStatusConnector);
if (!_userStatusConnector) {
return;
}
clearError();
_userStatusConnector->setUserStatus(_userStatus);
}
void UserStatusSelectorModel::clearUserStatus()
{
Q_ASSERT(_userStatusConnector);
if (!_userStatusConnector) {
return;
}
clearError();
_userStatusConnector->clearMessage();
}
void UserStatusSelectorModel::onPredefinedStatusesFetched(const std::vector<UserStatus> &statuses)
{
_predefinedStatuses = statuses;
emit predefinedStatusesChanged();
}
UserStatus UserStatusSelectorModel::predefinedStatus(int index) const
{
Q_ASSERT(0 <= index && index < static_cast<int>(_predefinedStatuses.size()));
return _predefinedStatuses[index];
}
int UserStatusSelectorModel::predefinedStatusesCount() const
{
return static_cast<int>(_predefinedStatuses.size());
}
void UserStatusSelectorModel::setPredefinedStatus(int index)
{
Q_ASSERT(0 <= index && index < static_cast<int>(_predefinedStatuses.size()));
_userStatus.setMessagePredefined(true);
const auto predefinedStatus = _predefinedStatuses[index];
_userStatus.setId(predefinedStatus.id());
_userStatus.setMessage(predefinedStatus.message());
_userStatus.setIcon(predefinedStatus.icon());
_userStatus.setClearAt(predefinedStatus.clearAt());
emit userStatusChanged();
emit clearAtChanged();
}
QString UserStatusSelectorModel::clearAtStageToString(ClearStageType stage) const
{
switch (stage) {
case ClearStageType::DontClear:
return tr("Don't clear");
case ClearStageType::HalfHour:
return tr("30 minutes");
case ClearStageType::OneHour:
return tr("1 hour");
case ClearStageType::FourHour:
return tr("4 hours");
case ClearStageType::Today:
return tr("Today");
case ClearStageType::Week:
return tr("This week");
default:
Q_UNREACHABLE();
}
}
QStringList UserStatusSelectorModel::clearAtValues() const
{
QStringList clearAtStages;
std::transform(_clearStages.begin(), _clearStages.end(),
std::back_inserter(clearAtStages),
[this](const ClearStageType &stage) { return clearAtStageToString(stage); });
return clearAtStages;
}
void UserStatusSelectorModel::setClearAt(int index)
{
Q_ASSERT(0 <= index && index < static_cast<int>(_clearStages.size()));
_userStatus.setClearAt(clearStageTypeToDateTime(_clearStages[index]));
emit clearAtChanged();
}
QString UserStatusSelectorModel::errorMessage() const
{
return _errorMessage;
}
QString UserStatusSelectorModel::timeDifferenceToString(int differenceSecs) const
{
if (differenceSecs < 60) {
return tr("Less than a minute");
} else if (differenceSecs < 60 * 60) {
const auto minutesLeft = std::ceil(differenceSecs / 60.0);
if (minutesLeft == 1) {
return tr("1 minute");
} else {
return tr("%1 minutes").arg(minutesLeft);
}
} else if (differenceSecs < 60 * 60 * 24) {
const auto hoursLeft = std::ceil(differenceSecs / 60.0 / 60.0);
if (hoursLeft == 1) {
return tr("1 hour");
} else {
return tr("%1 hours").arg(hoursLeft);
}
} else {
const auto daysLeft = std::ceil(differenceSecs / 60.0 / 60.0 / 24.0);
if (daysLeft == 1) {
return tr("1 day");
} else {
return tr("%1 days").arg(daysLeft);
}
}
}
QString UserStatusSelectorModel::clearAtReadable(const Optional<ClearAt> &clearAt) const
{
if (clearAt) {
switch (clearAt->_type) {
case ClearAtType::Period: {
return timeDifferenceToString(clearAt->_period);
}
case ClearAtType::Timestamp: {
const int difference = static_cast<int>(clearAt->_timestamp - _dateTimeProvider->currentDateTime().toTime_t());
return timeDifferenceToString(difference);
}
case ClearAtType::EndOf: {
if (clearAt->_endof == "day") {
return tr("Today");
} else if (clearAt->_endof == "week") {
return tr("This week");
}
Q_UNREACHABLE();
}
default:
Q_UNREACHABLE();
}
}
return tr("Don't clear");
}
QString UserStatusSelectorModel::predefinedStatusClearAt(int index) const
{
return clearAtReadable(predefinedStatus(index).clearAt());
}
QString UserStatusSelectorModel::clearAt() const
{
return clearAtReadable(_userStatus.clearAt());
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "common/result.h"
#include <userstatusconnector.h>
#include <datetimeprovider.h>
#include <QObject>
#include <QMetaType>
#include <QtNumeric>
#include <cstddef>
#include <memory>
#include <vector>
namespace OCC {
class UserStatusSelectorModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString userStatusMessage READ userStatusMessage NOTIFY userStatusChanged)
Q_PROPERTY(QString userStatusEmoji READ userStatusEmoji WRITE setUserStatusEmoji NOTIFY userStatusChanged)
Q_PROPERTY(OCC::UserStatus::OnlineStatus onlineStatus READ onlineStatus WRITE setOnlineStatus NOTIFY onlineStatusChanged)
Q_PROPERTY(int predefinedStatusesCount READ predefinedStatusesCount NOTIFY predefinedStatusesChanged)
Q_PROPERTY(QStringList clearAtValues READ clearAtValues CONSTANT)
Q_PROPERTY(QString clearAt READ clearAt NOTIFY clearAtChanged)
Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
Q_PROPERTY(QUrl onlineIcon READ onlineIcon CONSTANT)
Q_PROPERTY(QUrl awayIcon READ awayIcon CONSTANT)
Q_PROPERTY(QUrl dndIcon READ dndIcon CONSTANT)
Q_PROPERTY(QUrl invisibleIcon READ invisibleIcon CONSTANT)
public:
explicit UserStatusSelectorModel(QObject *parent = nullptr);
explicit UserStatusSelectorModel(std::shared_ptr<UserStatusConnector> userStatusConnector,
QObject *parent = nullptr);
explicit UserStatusSelectorModel(std::shared_ptr<UserStatusConnector> userStatusConnector,
std::unique_ptr<DateTimeProvider> dateTimeProvider,
QObject *parent = nullptr);
explicit UserStatusSelectorModel(const UserStatus &userStatus,
std::unique_ptr<DateTimeProvider> dateTimeProvider,
QObject *parent = nullptr);
explicit UserStatusSelectorModel(const UserStatus &userStatus,
QObject *parent = nullptr);
~UserStatusSelectorModel() override;
Q_INVOKABLE void load(int id);
Q_REQUIRED_RESULT UserStatus::OnlineStatus onlineStatus() const;
Q_INVOKABLE void setOnlineStatus(OCC::UserStatus::OnlineStatus status);
Q_REQUIRED_RESULT QUrl onlineIcon() const;
Q_REQUIRED_RESULT QUrl awayIcon() const;
Q_REQUIRED_RESULT QUrl dndIcon() const;
Q_REQUIRED_RESULT QUrl invisibleIcon() const;
Q_REQUIRED_RESULT QString userStatusMessage() const;
Q_INVOKABLE void setUserStatusMessage(const QString &message);
void setUserStatusEmoji(const QString &emoji);
Q_REQUIRED_RESULT QString userStatusEmoji() const;
Q_INVOKABLE void setUserStatus();
Q_INVOKABLE void clearUserStatus();
Q_REQUIRED_RESULT int predefinedStatusesCount() const;
Q_INVOKABLE UserStatus predefinedStatus(int index) const;
Q_INVOKABLE QString predefinedStatusClearAt(int index) const;
Q_INVOKABLE void setPredefinedStatus(int index);
Q_REQUIRED_RESULT QStringList clearAtValues() const;
Q_REQUIRED_RESULT QString clearAt() const;
Q_INVOKABLE void setClearAt(int index);
Q_REQUIRED_RESULT QString errorMessage() const;
signals:
void errorMessageChanged();
void userStatusChanged();
void onlineStatusChanged();
void clearAtChanged();
void predefinedStatusesChanged();
void finished();
private:
enum class ClearStageType {
DontClear,
HalfHour,
OneHour,
FourHour,
Today,
Week
};
void init();
void reset();
void onUserStatusFetched(const UserStatus &userStatus);
void onPredefinedStatusesFetched(const std::vector<UserStatus> &statuses);
void onUserStatusSet();
void onMessageCleared();
void onError(UserStatusConnector::Error error);
Q_REQUIRED_RESULT QString clearAtStageToString(ClearStageType stage) const;
Q_REQUIRED_RESULT QString clearAtReadable(const Optional<ClearAt> &clearAt) const;
Q_REQUIRED_RESULT QString timeDifferenceToString(int differenceSecs) const;
Q_REQUIRED_RESULT Optional<ClearAt> clearStageTypeToDateTime(ClearStageType type) const;
void setError(const QString &reason);
void clearError();
std::shared_ptr<UserStatusConnector> _userStatusConnector {};
std::vector<UserStatus> _predefinedStatuses;
UserStatus _userStatus;
std::unique_ptr<DateTimeProvider> _dateTimeProvider;
QString _errorMessage;
std::vector<ClearStageType> _clearStages = {
ClearStageType::DontClear,
ClearStageType::HalfHour,
ClearStageType::OneHour,
ClearStageType::FourHour,
ClearStageType::Today,
ClearStageType::Week
};
};
}

View File

@ -55,6 +55,9 @@ set(libsync_SRCS
theme.cpp
clientsideencryption.cpp
clientsideencryptionjobs.cpp
datetimeprovider.cpp
ocsuserstatusconnector.cpp
userstatusconnector.cpp
creds/dummycredentials.cpp
creds/abstractcredentials.cpp
creds/credentialscommon.cpp

View File

@ -139,6 +139,15 @@ QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUr
return reply;
}
QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUrl &url,
QNetworkRequest req, const QByteArray &requestBody)
{
auto reply = _account->sendRawRequest(verb, url, req, requestBody);
_requestBody = nullptr;
adoptRequest(reply);
return reply;
}
void AbstractNetworkJob::adoptRequest(QNetworkReply *reply)
{
addTimer(reply);

View File

@ -128,6 +128,9 @@ protected:
QNetworkRequest req = QNetworkRequest(),
QIODevice *requestBody = nullptr);
QNetworkReply *sendRequest(const QByteArray &verb, const QUrl &url,
QNetworkRequest req, const QByteArray &requestBody);
// sendRequest does not take a relative path instead of an url,
// but the old API allowed that. We have this undefined overload
// to help catch usage errors

View File

@ -13,6 +13,8 @@
*/
#include "account.h"
#include "accountfwd.h"
#include "clientsideencryptionjobs.h"
#include "cookiejar.h"
#include "networkjobs.h"
#include "configfile.h"
@ -27,6 +29,7 @@
#include "common/asserts.h"
#include "clientsideencryption.h"
#include "ocsuserstatusconnector.h"
#include <QLoggingCategory>
#include <QNetworkReply>
@ -43,6 +46,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QLoggingCategory>
#include <qsslconfiguration.h>
#include <qt5keychain/keychain.h>
@ -93,6 +97,7 @@ QString Account::davPath() const
void Account::setSharedThis(AccountPtr sharedThis)
{
_sharedThis = sharedThis.toWeakRef();
setupUserStatusConnector();
}
QString Account::davPathBase()
@ -337,6 +342,24 @@ QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url,
return _am->sendCustomRequest(req, verb, data);
}
QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, const QByteArray &data)
{
req.setUrl(url);
req.setSslConfiguration(this->getOrCreateSslConfig());
if (verb == "HEAD" && data.isEmpty()) {
return _am->head(req);
} else if (verb == "GET" && data.isEmpty()) {
return _am->get(req);
} else if (verb == "POST") {
return _am->post(req, data);
} else if (verb == "PUT") {
return _am->put(req, data);
} else if (verb == "DELETE" && data.isEmpty()) {
return _am->deleteResource(req);
}
return _am->sendCustomRequest(req, verb, data);
}
SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
{
auto job = new SimpleNetworkJob(sharedFromThis());
@ -544,9 +567,18 @@ void Account::setCapabilities(const QVariantMap &caps)
{
_capabilities = Capabilities(caps);
setupUserStatusConnector();
trySetupPushNotifications();
}
void Account::setupUserStatusConnector()
{
_userStatusConnector = std::make_shared<OcsUserStatusConnector>(sharedFromThis());
connect(_userStatusConnector.get(), &UserStatusConnector::userStatusFetched, this, [this](const UserStatus &) {
emit userStatusChanged();
});
}
QString Account::serverVersion() const
{
return _serverVersion;
@ -744,4 +776,9 @@ PushNotifications *Account::pushNotifications() const
return _pushNotifications;
}
std::shared_ptr<UserStatusConnector> Account::userStatusConnector() const
{
return _userStatusConnector;
}
} // namespace OCC

View File

@ -55,6 +55,7 @@ using AccountPtr = QSharedPointer<Account>;
class AccessManager;
class SimpleNetworkJob;
class PushNotifications;
class UserStatusConnector;
/**
* @brief Reimplement this to handle SSL errors from libsync
@ -150,6 +151,9 @@ public:
QNetworkRequest req = QNetworkRequest(),
QIODevice *data = nullptr);
QNetworkReply *sendRawRequest(const QByteArray &verb,
const QUrl &url, QNetworkRequest req, const QByteArray &data);
/** Create and start network job for a simple one-off request.
*
* More complicated requests typically create their own job types.
@ -251,10 +255,13 @@ public:
// Check for the directEditing capability
void fetchDirectEditors(const QUrl &directEditingURL, const QString &directEditingETag);
void setupUserStatusConnector();
void trySetupPushNotifications();
PushNotifications *pushNotifications() const;
void setPushNotificationsReconnectInterval(int interval);
std::shared_ptr<UserStatusConnector> userStatusConnector() const;
public slots:
/// Used when forgetting credentials
void clearQNAMCache();
@ -287,6 +294,8 @@ signals:
void pushNotificationsReady(Account *account);
void pushNotificationsDisabled(Account *account);
void userStatusChanged();
protected Q_SLOTS:
void slotCredentialsFetched();
void slotCredentialsAsked();
@ -343,6 +352,8 @@ private:
PushNotifications *_pushNotifications = nullptr;
std::shared_ptr<UserStatusConnector> _userStatusConnector;
/* IMPORTANT - remove later - FIXME MS@2019-12-07 -->
* TODO: For "Log out" & "Remove account": Remove client CA certs and KEY!
*

View File

@ -187,13 +187,31 @@ bool Capabilities::chunkingNg() const
return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0";
}
bool Capabilities::userStatus() const
bool Capabilities::userStatusNotification() const
{
return _capabilities.contains("notifications") &&
_capabilities["notifications"].toMap().contains("ocs-endpoints") &&
_capabilities["notifications"].toMap()["ocs-endpoints"].toStringList().contains("user-status");
}
bool Capabilities::userStatus() const
{
if (!_capabilities.contains("user_status")) {
return false;
}
const auto userStatusMap = _capabilities["user_status"].toMap();
return userStatusMap.value("enabled", false).toBool();
}
bool Capabilities::userStatusSupportsEmoji() const
{
if (!userStatus()) {
return false;
}
const auto userStatusMap = _capabilities["user_status"].toMap();
return userStatusMap.value("supports_emoji", false).toBool();
}
PushNotificationTypes Capabilities::availablePushNotifications() const
{
if (!_capabilities.contains("notify_push")) {

View File

@ -58,7 +58,9 @@ public:
bool sharePublicLinkMultiple() const;
bool shareResharing() const;
bool chunkingNg() const;
bool userStatusNotification() const;
bool userStatus() const;
bool userStatusSupportsEmoji() const;
/// Returns which kind of push notfications are available
PushNotificationTypes availablePushNotifications() const;

View File

@ -56,7 +56,8 @@ Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg)
Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg)
Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg)
QString baseUrl(){
QString e2eeBaseUrl()
{
return QStringLiteral("ocs/v2.php/apps/end_to_end_encryption/api/v1/");
}
@ -1180,7 +1181,7 @@ void ClientSideEncryption::generateCSR(const AccountPtr &account, EVP_PKEY *keyP
qCInfo(lcCse()) << "Returning the certificate";
qCInfo(lcCse()) << output;
auto job = new SignPublicKeyApiJob(account, baseUrl() + "public-key", this);
auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl() + "public-key", this);
job->setCsr(output);
connect(job, &SignPublicKeyApiJob::jsonReceived, [this, account](const QJsonDocument& json, int retCode) {
@ -1212,7 +1213,7 @@ void ClientSideEncryption::encryptPrivateKey(const AccountPtr &account)
auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt);
// Send private key to the server
auto job = new StorePrivateKeyApiJob(account, baseUrl() + "private-key", this);
auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl() + "private-key", this);
job->setPrivateKey(cryptedText);
connect(job, &StorePrivateKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
Q_UNUSED(doc);
@ -1296,7 +1297,7 @@ void ClientSideEncryption::decryptPrivateKey(const AccountPtr &account, const QB
void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account)
{
qCInfo(lcCse()) << "Retrieving private key from server";
auto job = new JsonApiJob(account, baseUrl() + "private-key", this);
auto job = new JsonApiJob(account, e2eeBaseUrl() + "private-key", this);
connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
if (retCode == 200) {
QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString();
@ -1315,7 +1316,7 @@ void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account)
void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account)
{
qCInfo(lcCse()) << "Retrieving public key from server";
auto job = new JsonApiJob(account, baseUrl() + "public-key", this);
auto job = new JsonApiJob(account, e2eeBaseUrl() + "public-key", this);
connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
if (retCode == 200) {
QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString();
@ -1336,7 +1337,7 @@ void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account)
void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr &account)
{
qCInfo(lcCse()) << "Retrieving public key from server";
auto job = new JsonApiJob(account, baseUrl() + "server-key", this);
auto job = new JsonApiJob(account, e2eeBaseUrl() + "server-key", this);
connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
if (retCode == 200) {
const auto serverPublicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-key"].toString().toLatin1();

View File

@ -23,7 +23,7 @@ class ReadPasswordJob;
namespace OCC {
QString baseUrl();
QString e2eeBaseUrl();
namespace EncryptionHelper {
QByteArray generateRandomFilename();

View File

@ -27,7 +27,7 @@ namespace OCC {
GetMetadataApiJob::GetMetadataApiJob(const AccountPtr& account,
const QByteArray& fileId,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
{
}
@ -63,7 +63,7 @@ StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account,
const QByteArray& fileId,
const QByteArray& b64Metadata,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata)
{
}
@ -104,8 +104,8 @@ UpdateMetadataApiJob::UpdateMetadataApiJob(const AccountPtr& account,
const QByteArray& b64Metadata,
const QByteArray& token,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent),
_fileId(fileId),
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent)
, _fileId(fileId),
_b64Metadata(b64Metadata),
_token(token)
{
@ -154,7 +154,7 @@ UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account,
const QByteArray& fileId,
const QByteArray& token,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token)
{
}
@ -185,11 +185,10 @@ bool UnlockEncryptFolderApiJob::finished()
}
DeleteMetadataApiJob::DeleteMetadataApiJob(const AccountPtr& account,
const QByteArray& fileId,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
{
}
@ -219,7 +218,7 @@ bool DeleteMetadataApiJob::finished()
}
LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId)
{
}
@ -258,7 +257,7 @@ bool LockEncryptFolderApiJob::finished()
}
SetEncryptionFlagApiJob::SetEncryptionFlagApiJob(const AccountPtr& account, const QByteArray& fileId, FlagAction flagAction, QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction)
: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction)
{
}

View File

@ -0,0 +1,17 @@
#include "datetimeprovider.h"
namespace OCC {
DateTimeProvider::~DateTimeProvider() = default;
QDateTime DateTimeProvider::currentDateTime() const
{
return QDateTime::currentDateTime();
}
QDate DateTimeProvider::currentDate() const
{
return QDate::currentDate();
}
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "owncloudlib.h"
#include <QDateTime>
namespace OCC {
class OWNCLOUDSYNC_EXPORT DateTimeProvider
{
public:
virtual ~DateTimeProvider();
virtual QDateTime currentDateTime() const;
virtual QDate currentDate() const;
};
}

View File

@ -830,13 +830,49 @@ void JsonApiJob::addRawHeader(const QByteArray &headerName, const QByteArray &va
_request.setRawHeader(headerName, value);
}
void JsonApiJob::setBody(const QJsonDocument &body)
{
_body = body.toJson();
qCDebug(lcJsonApiJob) << "Set body for request:" << _body;
if (!_body.isEmpty()) {
_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
}
void JsonApiJob::setVerb(Verb value)
{
_verb = value;
}
QByteArray JsonApiJob::verbToString() const
{
switch (_verb) {
case Verb::Get:
return "GET";
case Verb::Post:
return "POST";
case Verb::Put:
return "PUT";
case Verb::Delete:
return "DELETE";
}
return "GET";
}
void JsonApiJob::start()
{
addRawHeader("OCS-APIREQUEST", "true");
auto query = _additionalParams;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path(), query);
sendRequest(_usePOST ? "POST" : "GET", url, _request);
const auto httpVerb = verbToString();
if (!_body.isEmpty()) {
sendRequest(httpVerb, url, _request, _body);
} else {
sendRequest(httpVerb, url, _request);
}
AbstractNetworkJob::start();
}

View File

@ -22,6 +22,7 @@
#include <QBuffer>
#include <QUrlQuery>
#include <QJsonDocument>
#include <functional>
class QUrl;
@ -375,6 +376,13 @@ class OWNCLOUDSYNC_EXPORT JsonApiJob : public AbstractNetworkJob
{
Q_OBJECT
public:
enum class Verb {
Get,
Post,
Put,
Delete,
};
explicit JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);
/**
@ -390,15 +398,9 @@ public:
void addQueryParams(const QUrlQuery &params);
void addRawHeader(const QByteArray &headerName, const QByteArray &value);
/**
* @brief usePOST - allow job to do an anonymous POST request instead of GET
* @param params: (optional) true for POST, false for GET (default).
*
* This function needs to be called before start() obviously.
*/
void usePOST(bool usePOST = true) {
_usePOST = usePOST;
}
void setBody(const QJsonDocument &body);
void setVerb(Verb value);
public slots:
void start() override;
@ -429,10 +431,13 @@ signals:
void allowDesktopNotificationsChanged(bool isAllowed);
private:
QByteArray _body;
QUrlQuery _additionalParams;
QNetworkRequest _request;
bool _usePOST = false;
Verb _verb = Verb::Get;
QByteArray verbToString() const;
};
/**

View File

@ -0,0 +1,455 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "ocsuserstatusconnector.h"
#include "account.h"
#include "userstatusconnector.h"
#include <networkjobs.h>
#include <QDateTime>
#include <QtGlobal>
#include <QJsonDocument>
#include <QJsonValue>
#include <QLoggingCategory>
#include <QString>
#include <QJsonObject>
#include <QJsonArray>
#include <qdatetime.h>
#include <qjsonarray.h>
#include <qjsonobject.h>
#include <qloggingcategory.h>
namespace {
Q_LOGGING_CATEGORY(lcOcsUserStatusConnector, "nextcloud.gui.ocsuserstatusconnector", QtInfoMsg)
OCC::UserStatus::OnlineStatus stringToUserOnlineStatus(const QString &status)
{
// it needs to match the Status enum
const QHash<QString, OCC::UserStatus::OnlineStatus> preDefinedStatus {
{ "online", OCC::UserStatus::OnlineStatus::Online },
{ "dnd", OCC::UserStatus::OnlineStatus::DoNotDisturb },
{ "away", OCC::UserStatus::OnlineStatus::Away },
{ "offline", OCC::UserStatus::OnlineStatus::Offline },
{ "invisible", OCC::UserStatus::OnlineStatus::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(), OCC::UserStatus::OnlineStatus::Online);
}
QString onlineStatusToString(OCC::UserStatus::OnlineStatus status)
{
switch (status) {
case OCC::UserStatus::OnlineStatus::Online:
return QStringLiteral("online");
case OCC::UserStatus::OnlineStatus::DoNotDisturb:
return QStringLiteral("dnd");
case OCC::UserStatus::OnlineStatus::Away:
return QStringLiteral("offline");
case OCC::UserStatus::OnlineStatus::Offline:
return QStringLiteral("offline");
case OCC::UserStatus::OnlineStatus::Invisible:
return QStringLiteral("invisible");
}
return QStringLiteral("online");
}
OCC::Optional<OCC::ClearAt> jsonExtractClearAt(QJsonObject jsonObject)
{
OCC::Optional<OCC::ClearAt> clearAt {};
if (jsonObject.contains("clearAt") && !jsonObject.value("clearAt").isNull()) {
OCC::ClearAt clearAtValue;
clearAtValue._type = OCC::ClearAtType::Timestamp;
clearAtValue._timestamp = jsonObject.value("clearAt").toInt();
clearAt = clearAtValue;
}
return clearAt;
}
OCC::UserStatus jsonExtractUserStatus(QJsonObject json)
{
const auto clearAt = jsonExtractClearAt(json);
const OCC::UserStatus userStatus(json.value("messageId").toString(),
json.value("message").toString().trimmed(),
json.value("icon").toString().trimmed(), stringToUserOnlineStatus(json.value("status").toString()),
json.value("messageIsPredefined").toBool(false), clearAt);
return userStatus;
}
OCC::UserStatus jsonToUserStatus(const QJsonDocument &json)
{
const QJsonObject defaultValues {
{ "icon", "" },
{ "message", "" },
{ "status", "online" },
{ "messageIsPredefined", "false" },
{ "statusIsUserDefined", "false" }
};
const auto retrievedData = json.object().value("ocs").toObject().value("data").toObject(defaultValues);
return jsonExtractUserStatus(retrievedData);
}
quint64 clearAtEndOfToTimestamp(const OCC::ClearAt &clearAt)
{
Q_ASSERT(clearAt._type == OCC::ClearAtType::EndOf);
if (clearAt._endof == "day") {
return QDate::currentDate().addDays(1).startOfDay().toTime_t();
} else if (clearAt._endof == "week") {
const auto days = Qt::Sunday - QDate::currentDate().dayOfWeek();
return QDate::currentDate().addDays(days + 1).startOfDay().toTime_t();
}
qCWarning(lcOcsUserStatusConnector) << "Can not handle clear at endof day type" << clearAt._endof;
return QDateTime::currentDateTime().toTime_t();
}
quint64 clearAtPeriodToTimestamp(const OCC::ClearAt &clearAt)
{
return QDateTime::currentDateTime().addSecs(clearAt._period).toTime_t();
}
quint64 clearAtToTimestamp(const OCC::ClearAt &clearAt)
{
switch (clearAt._type) {
case OCC::ClearAtType::Period: {
return clearAtPeriodToTimestamp(clearAt);
}
case OCC::ClearAtType::EndOf: {
return clearAtEndOfToTimestamp(clearAt);
}
case OCC::ClearAtType::Timestamp: {
return clearAt._timestamp;
}
}
return 0;
}
quint64 clearAtToTimestamp(const OCC::Optional<OCC::ClearAt> &clearAt)
{
if (clearAt) {
return clearAtToTimestamp(*clearAt);
}
return 0;
}
OCC::Optional<OCC::ClearAt> jsonToClearAt(QJsonObject jsonObject)
{
OCC::Optional<OCC::ClearAt> clearAt;
if (jsonObject.value("clearAt").isObject() && !jsonObject.value("clearAt").isNull()) {
OCC::ClearAt clearAtValue;
const auto clearAtObject = jsonObject.value("clearAt").toObject();
const auto typeValue = clearAtObject.value("type").toString("period");
if (typeValue == "period") {
const auto timeValue = clearAtObject.value("time").toInt(0);
clearAtValue._type = OCC::ClearAtType::Period;
clearAtValue._period = timeValue;
} else if (typeValue == "end-of") {
const auto timeValue = clearAtObject.value("time").toString("day");
clearAtValue._type = OCC::ClearAtType::EndOf;
clearAtValue._endof = timeValue;
} else {
qCWarning(lcOcsUserStatusConnector) << "Can not handle clear type value" << typeValue;
}
clearAt = clearAtValue;
}
return clearAt;
}
OCC::UserStatus jsonToUserStatus(QJsonObject jsonObject)
{
const auto clearAt = jsonToClearAt(jsonObject);
OCC::UserStatus userStatus(
jsonObject.value("id").toString("no-id"),
jsonObject.value("message").toString("No message"),
jsonObject.value("icon").toString("no-icon"),
OCC::UserStatus::OnlineStatus::Online,
true,
clearAt);
return userStatus;
}
std::vector<OCC::UserStatus> jsonToPredefinedStatuses(QJsonArray jsonDataArray)
{
std::vector<OCC::UserStatus> statuses;
for (const auto &jsonEntry : jsonDataArray) {
Q_ASSERT(jsonEntry.isObject());
if (!jsonEntry.isObject()) {
continue;
}
statuses.push_back(jsonToUserStatus(jsonEntry.toObject()));
}
return statuses;
}
const QString baseUrl("/ocs/v2.php/apps/user_status/api/v1");
const QString userStatusBaseUrl = baseUrl + QStringLiteral("/user_status");
}
namespace OCC {
OcsUserStatusConnector::OcsUserStatusConnector(AccountPtr account, QObject *parent)
: UserStatusConnector(parent)
, _account(account)
{
Q_ASSERT(_account);
_userStatusSupported = _account->capabilities().userStatus();
_userStatusEmojisSupported = _account->capabilities().userStatusSupportsEmoji();
}
void OcsUserStatusConnector::fetchUserStatus()
{
qCDebug(lcOcsUserStatusConnector) << "Try to fetch user status";
if (!_userStatusSupported) {
qCDebug(lcOcsUserStatusConnector) << "User status not supported";
emit error(Error::UserStatusNotSupported);
return;
}
startFetchUserStatusJob();
}
void OcsUserStatusConnector::startFetchUserStatusJob()
{
if (_getUserStatusJob) {
qCDebug(lcOcsUserStatusConnector) << "Get user status job is already running.";
return;
}
_getUserStatusJob = new JsonApiJob(_account, userStatusBaseUrl, this);
connect(_getUserStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusFetched);
_getUserStatusJob->start();
}
void OcsUserStatusConnector::onUserStatusFetched(const QJsonDocument &json, int statusCode)
{
logResponse("user status fetched", json, statusCode);
if (statusCode != 200) {
qCInfo(lcOcsUserStatusConnector) << "Slot fetch UserStatus finished with status code" << statusCode;
emit error(Error::CouldNotFetchUserStatus);
return;
}
_userStatus = jsonToUserStatus(json);
emit userStatusFetched(_userStatus);
}
void OcsUserStatusConnector::startFetchPredefinedStatuses()
{
if (_getPredefinedStausesJob) {
qCDebug(lcOcsUserStatusConnector) << "Get predefined statuses job is already running";
return;
}
_getPredefinedStausesJob = new JsonApiJob(_account,
baseUrl + QStringLiteral("/predefined_statuses"), this);
connect(_getPredefinedStausesJob, &JsonApiJob::jsonReceived, this,
&OcsUserStatusConnector::onPredefinedStatusesFetched);
_getPredefinedStausesJob->start();
}
void OcsUserStatusConnector::fetchPredefinedStatuses()
{
if (!_userStatusSupported) {
emit error(Error::UserStatusNotSupported);
return;
}
startFetchPredefinedStatuses();
}
void OcsUserStatusConnector::onPredefinedStatusesFetched(const QJsonDocument &json, int statusCode)
{
logResponse("predefined statuses", json, statusCode);
if (statusCode != 200) {
qCInfo(lcOcsUserStatusConnector) << "Slot predefined user statuses finished with status code" << statusCode;
emit error(Error::CouldNotFetchPredefinedUserStatuses);
return;
}
const auto jsonData = json.object().value("ocs").toObject().value("data");
Q_ASSERT(jsonData.isArray());
if (!jsonData.isArray()) {
return;
}
const auto statuses = jsonToPredefinedStatuses(jsonData.toArray());
emit predefinedStatusesFetched(statuses);
}
void OcsUserStatusConnector::logResponse(const QString &message, const QJsonDocument &json, int statusCode)
{
qCDebug(lcOcsUserStatusConnector) << "Response from:" << message << "Status:" << statusCode << "Json:" << json;
}
void OcsUserStatusConnector::setUserStatusOnlineStatus(UserStatus::OnlineStatus onlineStatus)
{
_setOnlineStatusJob = new JsonApiJob(_account,
userStatusBaseUrl + QStringLiteral("/status"), this);
_setOnlineStatusJob->setVerb(JsonApiJob::Verb::Put);
// Set body
QJsonObject dataObject;
dataObject.insert("statusType", onlineStatusToString(onlineStatus));
QJsonDocument body;
body.setObject(dataObject);
_setOnlineStatusJob->setBody(body);
connect(_setOnlineStatusJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusOnlineStatusSet);
_setOnlineStatusJob->start();
}
void OcsUserStatusConnector::setUserStatusMessagePredefined(const UserStatus &userStatus)
{
Q_ASSERT(userStatus.messagePredefined());
if (!userStatus.messagePredefined()) {
return;
}
_setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/predefined"), this);
_setMessageJob->setVerb(JsonApiJob::Verb::Put);
// Set body
QJsonObject dataObject;
dataObject.insert("messageId", userStatus.id());
if (userStatus.clearAt()) {
dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(userStatus.clearAt())));
} else {
dataObject.insert("clearAt", QJsonValue());
}
QJsonDocument body;
body.setObject(dataObject);
_setMessageJob->setBody(body);
connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet);
_setMessageJob->start();
}
void OcsUserStatusConnector::setUserStatusMessageCustom(const UserStatus &userStatus)
{
Q_ASSERT(!userStatus.messagePredefined());
if (userStatus.messagePredefined()) {
return;
}
if (!_userStatusEmojisSupported) {
emit error(Error::EmojisNotSupported);
return;
}
_setMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message/custom"), this);
_setMessageJob->setVerb(JsonApiJob::Verb::Put);
// Set body
QJsonObject dataObject;
dataObject.insert("statusIcon", userStatus.icon());
dataObject.insert("message", userStatus.message());
const auto clearAt = userStatus.clearAt();
if (clearAt) {
dataObject.insert("clearAt", static_cast<int>(clearAtToTimestamp(*clearAt)));
} else {
dataObject.insert("clearAt", QJsonValue());
}
QJsonDocument body;
body.setObject(dataObject);
_setMessageJob->setBody(body);
connect(_setMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onUserStatusMessageSet);
_setMessageJob->start();
}
void OcsUserStatusConnector::setUserStatusMessage(const UserStatus &userStatus)
{
if (userStatus.messagePredefined()) {
setUserStatusMessagePredefined(userStatus);
return;
}
setUserStatusMessageCustom(userStatus);
}
void OcsUserStatusConnector::setUserStatus(const UserStatus &userStatus)
{
if (!_userStatusSupported) {
emit error(Error::UserStatusNotSupported);
return;
}
if (_setOnlineStatusJob || _setMessageJob) {
qCDebug(lcOcsUserStatusConnector) << "Set online status job or set message job are already running.";
return;
}
setUserStatusOnlineStatus(userStatus.state());
setUserStatusMessage(userStatus);
}
void OcsUserStatusConnector::onUserStatusOnlineStatusSet(const QJsonDocument &json, int statusCode)
{
logResponse("Online status set", json, statusCode);
if (statusCode != 200) {
emit error(Error::CouldNotSetUserStatus);
return;
}
}
void OcsUserStatusConnector::onUserStatusMessageSet(const QJsonDocument &json, int statusCode)
{
logResponse("Message set", json, statusCode);
if (statusCode != 200) {
emit error(Error::CouldNotSetUserStatus);
return;
}
// We fetch the user status again because json does not contain
// the new message when user status was set from a predefined
// message
fetchUserStatus();
emit userStatusSet();
}
void OcsUserStatusConnector::clearMessage()
{
_clearMessageJob = new JsonApiJob(_account, userStatusBaseUrl + QStringLiteral("/message"));
_clearMessageJob->setVerb(JsonApiJob::Verb::Delete);
connect(_clearMessageJob, &JsonApiJob::jsonReceived, this, &OcsUserStatusConnector::onMessageCleared);
_clearMessageJob->start();
}
UserStatus OcsUserStatusConnector::userStatus() const
{
return _userStatus;
}
void OcsUserStatusConnector::onMessageCleared(const QJsonDocument &json, int statusCode)
{
logResponse("Message cleared", json, statusCode);
if (statusCode != 200) {
emit error(Error::CouldNotClearMessage);
return;
}
_userStatus = {};
emit messageCleared();
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "accountfwd.h"
#include "userstatusconnector.h"
#include <QPointer>
namespace OCC {
class JsonApiJob;
class SimpleNetworkJob;
class OWNCLOUDSYNC_EXPORT OcsUserStatusConnector : public UserStatusConnector
{
public:
explicit OcsUserStatusConnector(AccountPtr account, QObject *parent = nullptr);
void fetchUserStatus() override;
void fetchPredefinedStatuses() override;
void setUserStatus(const UserStatus &userStatus) override;
void clearMessage() override;
UserStatus userStatus() const override;
private:
void onUserStatusFetched(const QJsonDocument &json, int statusCode);
void onPredefinedStatusesFetched(const QJsonDocument &json, int statusCode);
void onUserStatusOnlineStatusSet(const QJsonDocument &json, int statusCode);
void onUserStatusMessageSet(const QJsonDocument &json, int statusCode);
void onMessageCleared(const QJsonDocument &json, int statusCode);
void logResponse(const QString &message, const QJsonDocument &json, int statusCode);
void startFetchUserStatusJob();
void startFetchPredefinedStatuses();
void setUserStatusOnlineStatus(UserStatus::OnlineStatus onlineStatus);
void setUserStatusMessage(const UserStatus &userStatus);
void setUserStatusMessagePredefined(const UserStatus &userStatus);
void setUserStatusMessageCustom(const UserStatus &userStatus);
AccountPtr _account;
bool _userStatusSupported = false;
bool _userStatusEmojisSupported = false;
QPointer<JsonApiJob> _clearMessageJob {};
QPointer<JsonApiJob> _setMessageJob {};
QPointer<JsonApiJob> _setOnlineStatusJob {};
QPointer<JsonApiJob> _getPredefinedStausesJob {};
QPointer<JsonApiJob> _getUserStatusJob {};
UserStatus _userStatus;
};
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "userstatusconnector.h"
#include "theme.h"
namespace OCC {
UserStatus::UserStatus() = default;
UserStatus::UserStatus(
const QString &id, const QString &message, const QString &icon,
OnlineStatus state, bool messagePredefined, const Optional<ClearAt> &clearAt)
: _id(id)
, _message(message)
, _icon(icon)
, _state(state)
, _messagePredefined(messagePredefined)
, _clearAt(clearAt)
{
}
QString UserStatus::id() const
{
return _id;
}
QString UserStatus::message() const
{
return _message;
}
QString UserStatus::icon() const
{
return _icon;
}
auto UserStatus::state() const -> OnlineStatus
{
return _state;
}
bool UserStatus::messagePredefined() const
{
return _messagePredefined;
}
QUrl UserStatus::stateIcon() const
{
switch (_state) {
case UserStatus::OnlineStatus::Away:
return Theme::instance()->statusAwayImageSource();
case UserStatus::OnlineStatus::DoNotDisturb:
return Theme::instance()->statusDoNotDisturbImageSource();
case UserStatus::OnlineStatus::Invisible:
case UserStatus::OnlineStatus::Offline:
return Theme::instance()->statusInvisibleImageSource();
case UserStatus::OnlineStatus::Online:
return Theme::instance()->statusOnlineImageSource();
}
Q_UNREACHABLE();
}
Optional<ClearAt> UserStatus::clearAt() const
{
return _clearAt;
}
void UserStatus::setId(const QString &id)
{
_id = id;
}
void UserStatus::setMessage(const QString &message)
{
_message = message;
}
void UserStatus::setState(OnlineStatus state)
{
_state = state;
}
void UserStatus::setIcon(const QString &icon)
{
_icon = icon;
}
void UserStatus::setMessagePredefined(bool value)
{
_messagePredefined = value;
}
void UserStatus::setClearAt(const Optional<ClearAt> &dateTime)
{
_clearAt = dateTime;
}
UserStatusConnector::UserStatusConnector(QObject *parent)
: QObject(parent)
{
}
UserStatusConnector::~UserStatusConnector() = default;
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "common/result.h"
#include "owncloudlib.h"
#include <QObject>
#include <QString>
#include <QMetaType>
#include <QUrl>
#include <QDateTime>
#include <QtGlobal>
#include <QVariant>
#include <vector>
namespace OCC {
enum class OWNCLOUDSYNC_EXPORT ClearAtType {
Period,
EndOf,
Timestamp
};
// TODO: If we can use C++17 make it a std::variant
struct OWNCLOUDSYNC_EXPORT ClearAt
{
ClearAtType _type = ClearAtType::Period;
quint64 _timestamp;
int _period;
QString _endof;
};
class OWNCLOUDSYNC_EXPORT UserStatus
{
Q_GADGET
Q_PROPERTY(QString id MEMBER _id)
Q_PROPERTY(QString message MEMBER _message)
Q_PROPERTY(QString icon MEMBER _icon)
Q_PROPERTY(OnlineStatus state MEMBER _state)
public:
enum class OnlineStatus : quint8 {
Online,
DoNotDisturb,
Away,
Offline,
Invisible
};
Q_ENUM(OnlineStatus);
UserStatus();
UserStatus(const QString &id, const QString &message, const QString &icon,
OnlineStatus state, bool messagePredefined, const Optional<ClearAt> &clearAt = {});
Q_REQUIRED_RESULT QString id() const;
Q_REQUIRED_RESULT QString message() const;
Q_REQUIRED_RESULT QString icon() const;
Q_REQUIRED_RESULT OnlineStatus state() const;
Q_REQUIRED_RESULT Optional<ClearAt> clearAt() const;
void setId(const QString &id);
void setMessage(const QString &message);
void setState(OnlineStatus state);
void setIcon(const QString &icon);
void setMessagePredefined(bool value);
void setClearAt(const Optional<ClearAt> &dateTime);
Q_REQUIRED_RESULT bool messagePredefined() const;
Q_REQUIRED_RESULT QUrl stateIcon() const;
private:
QString _id;
QString _message;
QString _icon;
OnlineStatus _state = OnlineStatus::Online;
bool _messagePredefined;
Optional<ClearAt> _clearAt;
};
class OWNCLOUDSYNC_EXPORT UserStatusConnector : public QObject
{
Q_OBJECT
public:
enum class Error {
CouldNotFetchUserStatus,
CouldNotFetchPredefinedUserStatuses,
UserStatusNotSupported,
EmojisNotSupported,
CouldNotSetUserStatus,
CouldNotClearMessage
};
Q_ENUM(Error)
explicit UserStatusConnector(QObject *parent = nullptr);
~UserStatusConnector() override;
virtual void fetchUserStatus() = 0;
virtual void fetchPredefinedStatuses() = 0;
virtual void setUserStatus(const UserStatus &userStatus) = 0;
virtual void clearMessage() = 0;
virtual UserStatus userStatus() const = 0;
signals:
void userStatusFetched(const UserStatus &userStatus);
void predefinedStatusesFetched(const std::vector<UserStatus> &statuses);
void userStatusSet();
void messageCleared();
void error(Error error);
};
}
Q_DECLARE_METATYPE(OCC::UserStatusConnector *)
Q_DECLARE_METATYPE(OCC::UserStatus)

View File

@ -59,6 +59,7 @@ nextcloud_add_test(PushNotifications)
nextcloud_add_test(Theme)
nextcloud_add_test(IconUtils)
nextcloud_add_test(NotificationCache)
nextcloud_add_test(SetUserStatusDialog)
if( UNIX AND NOT APPLE )
nextcloud_add_test(InotifyWatcher)

View File

@ -138,6 +138,82 @@ private slots:
QCOMPARE(capabilities.pushNotificationsWebSocketUrl(), websocketUrl);
}
void testUserStatus_userStatusAvailable_returnTrue()
{
QVariantMap userStatusMap;
userStatusMap["enabled"] = true;
QVariantMap capabilitiesMap;
capabilitiesMap["user_status"] = userStatusMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(capabilities.userStatus());
}
void testUserStatus_userStatusNotAvailable_returnFalse()
{
QVariantMap userStatusMap;
userStatusMap["enabled"] = false;
QVariantMap capabilitiesMap;
capabilitiesMap["user_status"] = userStatusMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(!capabilities.userStatus());
}
void testUserStatus_userStatusNotInCapabilites_returnFalse()
{
QVariantMap capabilitiesMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(!capabilities.userStatus());
}
void testUserStatusSupportsEmoji_supportsEmojiAvailable_returnTrue()
{
QVariantMap userStatusMap;
userStatusMap["enabled"] = true;
userStatusMap["supports_emoji"] = true;
QVariantMap capabilitiesMap;
capabilitiesMap["user_status"] = userStatusMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(capabilities.userStatus());
}
void testUserStatusSupportsEmoji_supportsEmojiNotAvailable_returnFalse()
{
QVariantMap userStatusMap;
userStatusMap["enabled"] = true;
userStatusMap["supports_emoji"] = false;
QVariantMap capabilitiesMap;
capabilitiesMap["user_status"] = userStatusMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(!capabilities.userStatusSupportsEmoji());
}
void testUserStatusSupportsEmoji_supportsEmojiNotInCapabilites_returnFalse()
{
QVariantMap userStatusMap;
userStatusMap["enabled"] = true;
QVariantMap capabilitiesMap;
capabilitiesMap["user_status"] = userStatusMap;
const OCC::Capabilities capabilities(capabilitiesMap);
QVERIFY(!capabilities.userStatusSupportsEmoji());
}
};
QTEST_GUILESS_MAIN(TestCapabilities)

View File

@ -65,6 +65,20 @@ class TestPushNotifications : public QObject
Q_OBJECT
private slots:
void testTryReconnect_capabilitesReportPushNotificationsAvailable_reconnectForEver()
{
FakeWebSocketServer fakeServer;
auto account = FakeWebSocketServer::createAccount();
account->setPushNotificationsReconnectInterval(0);
// Let if fail a few times
QVERIFY(failThreeAuthenticationAttempts(fakeServer, account));
QVERIFY(failThreeAuthenticationAttempts(fakeServer, account));
// Push notifications should try to reconnect
QVERIFY(fakeServer.authenticateAccount(account));
}
void testSetup_correctCredentials_authenticateAndEmitReady()
{
FakeWebSocketServer fakeServer;
@ -272,20 +286,6 @@ private slots:
QVERIFY(verifyCalledOnceWithAccount(*activitiesChangedSpy, account));
}));
}
void testTryReconnect_capabilitesReportPushNotificationsAvailable_reconnectForEver()
{
FakeWebSocketServer fakeServer;
auto account = FakeWebSocketServer::createAccount();
account->setPushNotificationsReconnectInterval(0);
// Let if fail a few times
QVERIFY(failThreeAuthenticationAttempts(fakeServer, account));
QVERIFY(failThreeAuthenticationAttempts(fakeServer, account));
// Push notifications should try to reconnect
QVERIFY(fakeServer.authenticateAccount(account));
}
};
QTEST_GUILESS_MAIN(TestPushNotifications)

View File

@ -0,0 +1,747 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@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 "userstatusconnector.h"
#include "userstatusselectormodel.h"
#include <QTest>
#include <QSignalSpy>
#include <QDateTime>
#include <memory>
class FakeUserStatusConnector : public OCC::UserStatusConnector
{
public:
void fetchUserStatus() override
{
if (_couldNotFetchUserStatus) {
emit error(Error::CouldNotFetchUserStatus);
return;
} else if (_userStatusNotSupported) {
emit error(Error::UserStatusNotSupported);
return;
} else if (_emojisNotSupported) {
emit error(Error::EmojisNotSupported);
return;
}
emit userStatusFetched(_userStatus);
}
void fetchPredefinedStatuses() override
{
if (_couldNotFetchPredefinedUserStatuses) {
emit error(Error::CouldNotFetchPredefinedUserStatuses);
return;
}
emit predefinedStatusesFetched(_predefinedStatuses);
}
void setUserStatus(const OCC::UserStatus &userStatus) override
{
if (_couldNotSetUserStatusMessage) {
emit error(Error::CouldNotSetUserStatus);
return;
}
_userStatusSetByCallerOfSetUserStatus = userStatus;
emit UserStatusConnector::userStatusSet();
}
void clearMessage() override
{
if (_couldNotClearUserStatusMessage) {
emit error(Error::CouldNotClearMessage);
} else {
_isMessageCleared = true;
}
}
OCC::UserStatus userStatus() const override
{
return {}; // Not implemented
}
void setFakeUserStatus(const OCC::UserStatus &userStatus)
{
_userStatus = userStatus;
}
void setFakePredefinedStatuses(
const std::vector<OCC::UserStatus> &statuses)
{
_predefinedStatuses = statuses;
}
OCC::UserStatus userStatusSetByCallerOfSetUserStatus() const { return _userStatusSetByCallerOfSetUserStatus; }
bool messageCleared() const { return _isMessageCleared; }
void setErrorCouldNotFetchPredefinedUserStatuses(bool value)
{
_couldNotFetchPredefinedUserStatuses = value;
}
void setErrorCouldNotFetchUserStatus(bool value)
{
_couldNotFetchUserStatus = value;
}
void setErrorCouldNotSetUserStatusMessage(bool value)
{
_couldNotSetUserStatusMessage = value;
}
void setErrorUserStatusNotSupported(bool value)
{
_userStatusNotSupported = value;
}
void setErrorEmojisNotSupported(bool value)
{
_emojisNotSupported = value;
}
void setErrorCouldNotClearUserStatusMessage(bool value)
{
_couldNotClearUserStatusMessage = value;
}
private:
OCC::UserStatus _userStatusSetByCallerOfSetUserStatus;
OCC::UserStatus _userStatus;
std::vector<OCC::UserStatus> _predefinedStatuses;
bool _isMessageCleared = false;
bool _couldNotFetchPredefinedUserStatuses = false;
bool _couldNotFetchUserStatus = false;
bool _couldNotSetUserStatusMessage = false;
bool _userStatusNotSupported = false;
bool _emojisNotSupported = false;
bool _couldNotClearUserStatusMessage = false;
};
class FakeDateTimeProvider : public OCC::DateTimeProvider
{
public:
void setCurrentDateTime(const QDateTime &dateTime) { _dateTime = dateTime; }
QDateTime currentDateTime() const override { return _dateTime; }
QDate currentDate() const override { return _dateTime.date(); }
private:
QDateTime _dateTime;
};
static std::vector<OCC::UserStatus>
createFakePredefinedStatuses(const QDateTime &currentTime)
{
std::vector<OCC::UserStatus> statuses;
const QString userStatusId("fake-id");
const QString userStatusMessage("Predefined status");
const QString userStatusIcon("🏖");
const OCC::UserStatus::OnlineStatus userStatusState(OCC::UserStatus::OnlineStatus::Online);
const bool userStatusMessagePredefined(true);
OCC::Optional<OCC::ClearAt> userStatusClearAt;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(60 * 60).toTime_t();
userStatusClearAt = clearAt;
statuses.emplace_back(userStatusId, userStatusMessage, userStatusIcon,
userStatusState, userStatusMessagePredefined, userStatusClearAt);
return statuses;
}
static QDateTime createDateTime(int year = 2021, int month = 7, int day = 27,
int hour = 12, int minute = 0, int second = 0)
{
QDate fakeDate(year, month, day);
QTime fakeTime(hour, minute, second);
QDateTime fakeDateTime;
fakeDateTime.setDate(fakeDate);
fakeDateTime.setTime(fakeTime);
return fakeDateTime;
}
class TestSetUserStatusDialog : public QObject
{
Q_OBJECT
private slots:
void testCtor_fetchStatusAndPredefinedStatuses()
{
const QDateTime currentDateTime(QDateTime::currentDateTime());
const QString userStatusId("fake-id");
const QString userStatusMessage("Some status");
const QString userStatusIcon("");
const OCC::UserStatus::OnlineStatus userStatusState(OCC::UserStatus::OnlineStatus::DoNotDisturb);
const bool userStatusMessagePredefined(false);
OCC::Optional<OCC::ClearAt> userStatusClearAt;
{
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentDateTime.addDays(1).toTime_t();
userStatusClearAt = clearAt;
}
const OCC::UserStatus userStatus(userStatusId, userStatusMessage,
userStatusIcon, userStatusState, userStatusMessagePredefined, userStatusClearAt);
const auto fakePredefinedStatuses = createFakePredefinedStatuses(createDateTime());
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentDateTime);
fakeUserStatusJob->setFakeUserStatus(userStatus);
fakeUserStatusJob->setFakePredefinedStatuses(fakePredefinedStatuses);
OCC::UserStatusSelectorModel model(fakeUserStatusJob, std::move(fakeDateTimeProvider));
// Was user status set correctly?
QCOMPARE(model.userStatusMessage(), userStatusMessage);
QCOMPARE(model.userStatusEmoji(), userStatusIcon);
QCOMPARE(model.onlineStatus(), userStatusState);
QCOMPARE(model.clearAt(), tr("1 day"));
// Were predefined statuses fetched correctly?
const auto predefinedStatusesCount = model.predefinedStatusesCount();
QCOMPARE(predefinedStatusesCount, fakePredefinedStatuses.size());
for (int i = 0; i < predefinedStatusesCount; ++i) {
const auto predefinedStatus = model.predefinedStatus(i);
QCOMPARE(predefinedStatus.id(),
fakePredefinedStatuses[i].id());
QCOMPARE(predefinedStatus.message(),
fakePredefinedStatuses[i].message());
QCOMPARE(predefinedStatus.icon(),
fakePredefinedStatuses[i].icon());
QCOMPARE(predefinedStatus.messagePredefined(),
fakePredefinedStatuses[i].messagePredefined());
}
}
void testCtor_noStatusSet_showSensibleDefaults()
{
OCC::UserStatusSelectorModel model(nullptr, nullptr);
QCOMPARE(model.userStatusMessage(), "");
QCOMPARE(model.userStatusEmoji(), "😀");
QCOMPARE(model.clearAt(), tr("Don't clear"));
}
void testCtor_fetchStatusButNoStatusSet_showSensibleDefaults()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setFakeUserStatus({ "", "", "",
OCC::UserStatus::OnlineStatus::Offline, false, {} });
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.onlineStatus(), OCC::UserStatus::OnlineStatus::Online);
QCOMPARE(model.userStatusMessage(), "");
QCOMPARE(model.userStatusEmoji(), "😀");
QCOMPARE(model.clearAt(), tr("Don't clear"));
}
void testSetOnlineStatus_emitOnlineStatusChanged()
{
const OCC::UserStatus::OnlineStatus onlineStatus(OCC::UserStatus::OnlineStatus::Invisible);
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy onlineStatusChangedSpy(&model,
&OCC::UserStatusSelectorModel::onlineStatusChanged);
model.setOnlineStatus(onlineStatus);
QCOMPARE(onlineStatusChangedSpy.count(), 1);
}
void testSetUserStatus_setCustomMessage_userStatusSetCorrect()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy finishedSpy(&model, &OCC::UserStatusSelectorModel::finished);
const QString userStatusMessage("Some status");
const QString userStatusIcon("");
const OCC::UserStatus::OnlineStatus userStatusState(OCC::UserStatus::OnlineStatus::Online);
model.setOnlineStatus(userStatusState);
model.setUserStatusMessage(userStatusMessage);
model.setUserStatusEmoji(userStatusIcon);
model.setClearAt(1);
model.setUserStatus();
QCOMPARE(finishedSpy.count(), 1);
const auto userStatusSet = fakeUserStatusJob->userStatusSetByCallerOfSetUserStatus();
QCOMPARE(userStatusSet.icon(), userStatusIcon);
QCOMPARE(userStatusSet.message(), userStatusMessage);
QCOMPARE(userStatusSet.state(), userStatusState);
QCOMPARE(userStatusSet.messagePredefined(), false);
const auto clearAt = userStatusSet.clearAt();
QVERIFY(clearAt.isValid());
QCOMPARE(clearAt->_type, OCC::ClearAtType::Period);
QCOMPARE(clearAt->_period, 60 * 30);
}
void testSetUserStatusMessage_predefinedStatusWasSet_userStatusSetCorrect()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setFakePredefinedStatuses(createFakePredefinedStatuses(createDateTime()));
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
model.setPredefinedStatus(0);
QSignalSpy finishedSpy(&model, &OCC::UserStatusSelectorModel::finished);
const QString userStatusMessage("Some status");
const OCC::UserStatus::OnlineStatus userStatusState(OCC::UserStatus::OnlineStatus::Online);
model.setOnlineStatus(userStatusState);
model.setUserStatusMessage(userStatusMessage);
model.setClearAt(1);
model.setUserStatus();
QCOMPARE(finishedSpy.count(), 1);
const auto userStatusSet = fakeUserStatusJob->userStatusSetByCallerOfSetUserStatus();
QCOMPARE(userStatusSet.message(), userStatusMessage);
QCOMPARE(userStatusSet.state(), userStatusState);
QCOMPARE(userStatusSet.messagePredefined(), false);
const auto clearAt = userStatusSet.clearAt();
QVERIFY(clearAt.isValid());
QCOMPARE(clearAt->_type, OCC::ClearAtType::Period);
QCOMPARE(clearAt->_period, 60 * 30);
}
void testSetUserStatusEmoji_predefinedStatusWasSet_userStatusSetCorrect()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setFakePredefinedStatuses(createFakePredefinedStatuses(createDateTime()));
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
model.setPredefinedStatus(0);
QSignalSpy finishedSpy(&model, &OCC::UserStatusSelectorModel::finished);
const QString userStatusIcon("");
const OCC::UserStatus::OnlineStatus userStatusState(OCC::UserStatus::OnlineStatus::Online);
model.setOnlineStatus(userStatusState);
model.setUserStatusEmoji(userStatusIcon);
model.setClearAt(1);
model.setUserStatus();
QCOMPARE(finishedSpy.count(), 1);
const auto userStatusSet = fakeUserStatusJob->userStatusSetByCallerOfSetUserStatus();
QCOMPARE(userStatusSet.icon(), userStatusIcon);
QCOMPARE(userStatusSet.state(), userStatusState);
QCOMPARE(userStatusSet.messagePredefined(), false);
const auto clearAt = userStatusSet.clearAt();
QVERIFY(clearAt.isValid());
QCOMPARE(clearAt->_type, OCC::ClearAtType::Period);
QCOMPARE(clearAt->_period, 60 * 30);
}
void testSetPredefinedStatus_emitUserStatusChangedAndSetUserStatus()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
const auto currentTime = createDateTime();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
const auto fakePredefinedStatuses = createFakePredefinedStatuses(currentTime);
fakeUserStatusJob->setFakePredefinedStatuses(fakePredefinedStatuses);
OCC::UserStatusSelectorModel model(std::move(fakeUserStatusJob),
std::move(fakeDateTimeProvider));
QSignalSpy userStatusChangedSpy(&model,
&OCC::UserStatusSelectorModel::userStatusChanged);
QSignalSpy clearAtChangedSpy(&model,
&OCC::UserStatusSelectorModel::clearAtChanged);
const auto fakePredefinedUserStatusIndex = 0;
model.setPredefinedStatus(fakePredefinedUserStatusIndex);
QCOMPARE(userStatusChangedSpy.count(), 1);
QCOMPARE(clearAtChangedSpy.count(), 1);
// Was user status set correctly?
const auto fakePredefinedUserStatus = fakePredefinedStatuses[fakePredefinedUserStatusIndex];
QCOMPARE(model.userStatusMessage(), fakePredefinedUserStatus.message());
QCOMPARE(model.userStatusEmoji(), fakePredefinedUserStatus.icon());
QCOMPARE(model.onlineStatus(), fakePredefinedUserStatus.state());
QCOMPARE(model.clearAt(), tr("1 hour"));
}
void testSetClear_setClearAtStage0_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 0;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("Don't clear"));
}
void testSetClear_setClearAtStage1_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 1;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("30 minutes"));
}
void testSetClear_setClearAtStage2_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 2;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("1 hour"));
}
void testSetClear_setClearAtStage3_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 3;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("4 hours"));
}
void testSetClear_setClearAtStage4_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 4;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("Today"));
}
void testSetClear_setClearAtStage5_emitClearAtChangedAndClearAtSet()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QSignalSpy clearAtChangedSpy(&model, &OCC::UserStatusSelectorModel::clearAtChanged);
const auto clearAtIndex = 5;
model.setClearAt(clearAtIndex);
QCOMPARE(clearAtChangedSpy.count(), 1);
QCOMPARE(model.clearAt(), tr("This week"));
}
void testClearAtStages()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.clearAt(), tr("Don't clear"));
const auto clearAtValues = model.clearAtValues();
QCOMPARE(clearAtValues.count(), 6);
QCOMPARE(clearAtValues[0], tr("Don't clear"));
QCOMPARE(clearAtValues[1], tr("30 minutes"));
QCOMPARE(clearAtValues[2], tr("1 hour"));
QCOMPARE(clearAtValues[3], tr("4 hours"));
QCOMPARE(clearAtValues[4], tr("Today"));
QCOMPARE(clearAtValues[5], tr("This week"));
}
void testClearAt_clearAtTimestamp()
{
const auto currentTime = createDateTime();
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(30).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("Less than a minute"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(60).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("1 minute"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(60 * 30).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("30 minutes"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(60 * 60).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("1 hour"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addSecs(60 * 60 * 4).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("4 hours"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addDays(1).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("1 day"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Timestamp;
clearAt._timestamp = currentTime.addDays(7).toTime_t();
userStatus.setClearAt(clearAt);
auto fakeDateTimeProvider = std::make_unique<FakeDateTimeProvider>();
fakeDateTimeProvider->setCurrentDateTime(currentTime);
OCC::UserStatusSelectorModel model(userStatus, std::move(fakeDateTimeProvider));
QCOMPARE(model.clearAt(), tr("7 days"));
}
}
void testClearAt_clearAtEndOf()
{
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::EndOf;
clearAt._endof = "day";
userStatus.setClearAt(clearAt);
OCC::UserStatusSelectorModel model(userStatus);
QCOMPARE(model.clearAt(), tr("Today"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::EndOf;
clearAt._endof = "week";
userStatus.setClearAt(clearAt);
OCC::UserStatusSelectorModel model(userStatus);
QCOMPARE(model.clearAt(), tr("This week"));
}
}
void testClearAt_clearAtAfterPeriod()
{
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Period;
clearAt._period = 60 * 30;
userStatus.setClearAt(clearAt);
OCC::UserStatusSelectorModel model(userStatus);
QCOMPARE(model.clearAt(), tr("30 minutes"));
}
{
OCC::UserStatus userStatus;
OCC::ClearAt clearAt;
clearAt._type = OCC::ClearAtType::Period;
clearAt._period = 60 * 60;
userStatus.setClearAt(clearAt);
OCC::UserStatusSelectorModel model(userStatus);
QCOMPARE(model.clearAt(), tr("1 hour"));
}
}
void testClearUserStatus()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
model.clearUserStatus();
QVERIFY(fakeUserStatusJob->messageCleared());
}
void testError_couldNotFetchPredefinedStatuses_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorCouldNotFetchPredefinedUserStatuses(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.errorMessage(),
tr("Could not fetch predefined statuses. Make sure you are connected to the server."));
}
void testError_couldNotFetchUserStatus_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorCouldNotFetchUserStatus(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.errorMessage(),
tr("Could not fetch user status. Make sure you are connected to the server."));
}
void testError_userStatusNotSupported_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorUserStatusNotSupported(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.errorMessage(),
tr("User status feature is not supported. You will not be able to set your user status."));
}
void testError_couldSetUserStatus_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorCouldNotSetUserStatusMessage(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
model.setUserStatus();
QCOMPARE(model.errorMessage(),
tr("Could not set user status. Make sure you are connected to the server."));
}
void testError_emojisNotSupported_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorEmojisNotSupported(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
QCOMPARE(model.errorMessage(),
tr("Emojis feature is not supported. Some user status functionality may not work."));
}
void testError_couldNotClearMessage_emitError()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
fakeUserStatusJob->setErrorCouldNotClearUserStatusMessage(true);
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
model.clearUserStatus();
QCOMPARE(model.errorMessage(),
tr("Could not clear user status message. Make sure you are connected to the server."));
}
void testError_setUserStatus_clearErrorMessage()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
fakeUserStatusJob->setErrorCouldNotSetUserStatusMessage(true);
model.setUserStatus();
QVERIFY(!model.errorMessage().isEmpty());
fakeUserStatusJob->setErrorCouldNotSetUserStatusMessage(false);
model.setUserStatus();
QVERIFY(model.errorMessage().isEmpty());
}
void testError_clearUserStatus_clearErrorMessage()
{
auto fakeUserStatusJob = std::make_shared<FakeUserStatusConnector>();
OCC::UserStatusSelectorModel model(fakeUserStatusJob);
fakeUserStatusJob->setErrorCouldNotSetUserStatusMessage(true);
model.setUserStatus();
QVERIFY(!model.errorMessage().isEmpty());
fakeUserStatusJob->setErrorCouldNotSetUserStatusMessage(false);
model.clearUserStatus();
QVERIFY(model.errorMessage().isEmpty());
}
};
QTEST_GUILESS_MAIN(TestSetUserStatusDialog)
#include "testsetuserstatusdialog.moc"