Skip to content

Commit

Permalink
Merge pull request #4672 from nextcloud/bugfix/close-dead-call-notifi…
Browse files Browse the repository at this point in the history
…cations

Close call notifications when the call has been joined by the user, or the call has ended
  • Loading branch information
claucambra authored Jul 1, 2022
2 parents 974939e + a504dbf commit 8ce0842
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ set(client_SRCS
application.cpp
invalidfilenamedialog.h
invalidfilenamedialog.cpp
callstatechecker.h
callstatechecker.cpp
conflictdialog.h
conflictdialog.cpp
conflictsolver.h
Expand Down
184 changes: 184 additions & 0 deletions src/gui/callstatechecker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/

#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>

#include "callstatechecker.h"
#include "account.h"

namespace OCC {

Q_LOGGING_CATEGORY(lcCallStateChecker, "nextcloud.gui.callstatechecker", QtInfoMsg)

constexpr int successStatusCode = 200;

CallStateChecker::CallStateChecker(QObject *parent)
: QObject(parent)
{
setup();
}

void CallStateChecker::setup()
{
_notificationTimer.setSingleShot(true);
_notificationTimer.setInterval(60 * 1000);
connect(&_notificationTimer, &QTimer::timeout, this, &CallStateChecker::slotNotificationTimerElapsed);

_statusCheckTimer.setInterval(2 * 1000);
connect(&_statusCheckTimer, &QTimer::timeout, this, &CallStateChecker::slotStatusCheckTimerElapsed);
}

QString CallStateChecker::token() const
{
return _token;
}

void CallStateChecker::setToken(const QString &token)
{
_token = token;
Q_EMIT tokenChanged();
reset();
}

AccountState* CallStateChecker::accountState() const
{
return _accountState;
}

void CallStateChecker::setAccountState(AccountState *state)
{
_accountState = state;
Q_EMIT accountStateChanged();
reset();
}

bool CallStateChecker::checking() const
{
return _checking;
}

void CallStateChecker::setChecking(const bool checking)
{
if(checking) {
qCInfo(lcCallStateChecker) << "Starting to check state of call with token:" << _token;
_notificationTimer.start();
_statusCheckTimer.start();
} else {
qCInfo(lcCallStateChecker) << "Stopping checking of call state for call with token:" << _token;
_notificationTimer.stop();
_statusCheckTimer.stop();
_stateCheckJob.clear();
}

_checking = checking;
Q_EMIT checkingChanged();
}

void CallStateChecker::reset()
{
qCInfo(lcCallStateChecker, "Resetting call check");
setChecking(false);
setChecking(true);
}

void CallStateChecker::slotNotificationTimerElapsed()
{
qCInfo(lcCallStateChecker) << "Notification timer elapsed, stopping call checking of call with token:" << _token;
setChecking(false);
Q_EMIT stopNotifying();
}

void CallStateChecker::slotStatusCheckTimerElapsed()
{
// Don't run check if another check is still ongoing
if (_stateCheckJob) {
return;
}

startCallStateCheck();
}

bool CallStateChecker::isAccountServerVersion22OrLater() const
{
if(!_accountState || !_accountState->account()) {
return false;
}

const auto accountNcVersion = _accountState->account()->serverVersionInt();
constexpr auto ncVersion22 = OCC::Account::makeServerVersion(22, 0, 0);

return accountNcVersion >= ncVersion22;
}

void CallStateChecker::startCallStateCheck()
{
// check connectivity and credentials
if (!(_accountState && _accountState->isConnected() &&
_accountState->account() && _accountState->account()->credentials() &&
_accountState->account()->credentials()->ready())) {
qCInfo(lcCallStateChecker, "Could not connect, can't check call state.");
return;
}

// Check for token
if(_token.isEmpty()) {
qCInfo(lcCallStateChecker, "No call token set, can't check without it.");
return;
}

qCInfo(lcCallStateChecker) << "Checking state of call with token: " << _token;

const auto spreedPath = QStringLiteral("ocs/v2.php/apps/spreed/");
const auto callApiPath = isAccountServerVersion22OrLater() ? QStringLiteral("api/v4/call/") : QStringLiteral("api/v1/call/");
const QString callPath = spreedPath + callApiPath + _token; // Make sure it's a QString and not a QStringBuilder

_stateCheckJob = new JsonApiJob(_accountState->account(), callPath, this);
connect(_stateCheckJob.data(), &JsonApiJob::jsonReceived, this, &CallStateChecker::slotCallStateReceived);

_stateCheckJob->setVerb(JsonApiJob::Verb::Get);
_stateCheckJob->start();
}

void CallStateChecker::slotCallStateReceived(const QJsonDocument &json, const int statusCode)
{
if (statusCode != successStatusCode) {
qCInfo(lcCallStateChecker) << "Failed to retrieve call state data. Server returned status code: " << statusCode;
return;
}

const auto participantsJsonArray = json.object().value("ocs").toObject().value("data").toArray();

if (participantsJsonArray.empty()) {
qCInfo(lcCallStateChecker, "Call has no participants and has therefore been abandoned.");
Q_EMIT stopNotifying();
setChecking(false);
return;
}

for (const auto &participant : participantsJsonArray) {
const auto participantDataObject = participant.toObject();
const auto participantId = isAccountServerVersion22OrLater() ? participantDataObject.value("actorId").toString() : participantDataObject.value("userId").toString();

if (participantId == _accountState->account()->davUser()) {
qCInfo(lcCallStateChecker, "Found own account ID in participants list, meaning call has been joined.");
Q_EMIT stopNotifying();
setChecking(false);
return;
}
}
}

}
70 changes: 70 additions & 0 deletions src/gui/callstatechecker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/

#pragma once

#include <QObject>
#include <QTimer>

#include "networkjobs.h"
#include "accountstate.h"

namespace OCC {

class CallStateChecker : public QObject
{
Q_OBJECT
Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged)
Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
Q_PROPERTY(bool checking READ checking WRITE setChecking NOTIFY checkingChanged)

public:
explicit CallStateChecker(QObject *parent = nullptr);

QString token() const;
AccountState* accountState() const;
bool checking() const;

signals:
void tokenChanged();
void accountStateChanged();
void checkingChanged();

void stopNotifying();

public slots:
void setToken(const QString &token);
void setAccountState(OCC::AccountState *accountState);
void setChecking(const bool checking);

private slots:
void slotStatusCheckTimerElapsed();
void slotNotificationTimerElapsed();
void slotCallStateReceived(const QJsonDocument &json, const int statusCode);
void reset();

private:
void setup();
void startCallStateCheck();
bool isAccountServerVersion22OrLater() const;

AccountState *_accountState = nullptr;
QString _token;
QTimer _statusCheckTimer; // How often we check the status of the call
QTimer _notificationTimer; // How long we present the call notification for
QPointer<JsonApiJob> _stateCheckJob;
bool _checking = false;
};

}
5 changes: 4 additions & 1 deletion src/gui/systray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "tray/trayimageprovider.h"
#include "configfile.h"
#include "accessmanager.h"
#include "callstatechecker.h"

#include <QCursor>
#include <QGuiApplication>
Expand Down Expand Up @@ -98,6 +99,7 @@ Systray::Systray()
);

qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
qmlRegisterType<CallStateChecker>("com.nextcloud.desktopclient", 1, 0, "CallStateChecker");

#if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
setUserNotificationCenterDelegate();
Expand Down Expand Up @@ -183,7 +185,7 @@ void Systray::setupContextMenu()
});
}

void Systray::createCallDialog(const Activity &callNotification)
void Systray::createCallDialog(const Activity &callNotification, const AccountStatePtr accountState)
{
qCDebug(lcSystray) << "Starting a new call dialog for notification with id: " << callNotification._id << "with text: " << callNotification._subject;

Expand All @@ -208,6 +210,7 @@ void Systray::createCallDialog(const Activity &callNotification)
}

const QVariantMap initialProperties{
{"accountState", QVariant::fromValue(accountState.data())},
{"talkNotificationData", talkNotificationData},
{"links", links},
{"subject", callNotification._subject},
Expand Down
2 changes: 1 addition & 1 deletion src/gui/systray.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class Systray
bool isOpen();
QString windowTitle() const;
bool useNormalWindow() const;
void createCallDialog(const Activity &callNotification);
void createCallDialog(const Activity &callNotification, const AccountStatePtr accountState);

Q_INVOKABLE void pauseResumeSync();
Q_INVOKABLE bool syncIsPaused();
Expand Down
29 changes: 27 additions & 2 deletions src/gui/tray/CallNotificationDialog.qml
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
* Copyright (C) 2022 by Camila Ayres <camila@nextcloud.com>
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/

import QtQuick 2.15
import QtQuick.Window 2.15
import Style 1.0
Expand All @@ -20,6 +35,7 @@ Window {
readonly property string deleteIcon: svgImage.arg("delete")

// We set talkNotificationData, subject, and links properties in C++
property var accountState: ({})
property var talkNotificationData: ({})
property string subject: ""
property var links: []
Expand All @@ -29,6 +45,7 @@ Window {
readonly property bool usingUserAvatar: root.talkNotificationData.userAvatar !== ""

function closeNotification() {
callStateChecker.checking = false;
ringSound.stop();
root.close();
}
Expand All @@ -45,14 +62,22 @@ Window {
root.requestActivate();

ringSound.play();
}
callStateChecker.checking = true;
}

CallStateChecker {
id: callStateChecker
token: root.talkNotificationData.conversationToken
accountState: root.accountState

onStopNotifying: root.closeNotification()
}

Audio {
id: ringSound
source: root.ringtonePath
loops: 9 // about 45 seconds of audio playing
audioRole: Audio.RingtoneRole
onStopped: root.closeNotification()
}

Rectangle {
Expand Down
2 changes: 1 addition & 1 deletion src/gui/tray/usermodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ void User::slotBuildIncomingCallDialogs(const ActivityList &list)

if(systray) {
for(const auto &activity : list) {
systray->createCallDialog(activity);
systray->createCallDialog(activity, _account);
}
}
}
Expand Down
5 changes: 0 additions & 5 deletions src/libsync/account.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,6 @@ int Account::serverVersionInt() const
components.value(2).toInt());
}

int Account::makeServerVersion(int majorVersion, int minorVersion, int patchVersion)
{
return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
}

bool Account::serverVersionUnsupported() const
{
if (serverVersionInt() == 0) {
Expand Down
5 changes: 4 additions & 1 deletion src/libsync/account.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,10 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject
*/
int serverVersionInt() const;

static int makeServerVersion(int majorVersion, int minorVersion, int patchVersion);
static constexpr int makeServerVersion(const int majorVersion, const int minorVersion, const int patchVersion) {
return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
};

void setServerVersion(const QString &version);

/** Whether the server is too old.
Expand Down

0 comments on commit 8ce0842

Please sign in to comment.