feat(ui): add update notification enabled with -DUPDATE_CHECK

Fix #5335
reviewable/pr5346/r8
Anthony Bilinski 2019-01-07 21:46:30 -08:00
parent 52853485eb
commit 6c9d7b59c1
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
20 changed files with 422 additions and 64 deletions

View File

@ -180,7 +180,7 @@ build_qtox() {
rm -rf "$BUILDDIR"
echo '*** BUILDING "FULL" VERSION ***'
cmake -H. -B"$BUILDDIR"
cmake -H. -B"$BUILDDIR" -DUPDATE_CHECK=ON
bdir
}

View File

@ -11,6 +11,7 @@ option(PLATFORM_EXTENSIONS "Enable platform specific extensions, requires extra
option(USE_FILTERAUDIO "Enable the echo canceling backend" ON)
# AUTOUPDATE is currently broken and thus disabled
option(AUTOUPDATE "Enable the auto updater" OFF)
option(UPDATE_CHECK "Enable automatic update check" ON)
option(USE_CCACHE "Use ccache when available" ON)
option(SPELL_CHECK "Enable spell cheching support" ON)
option(ASAN "Compile with AddressSanitizer" OFF)
@ -632,6 +633,16 @@ if(${AUTOUPDATE} AND (WIN32 OR APPLE))
message(STATUS "using autoupdater")
endif()
if(${UPDATE_CHECK})
add_definitions(-DUPDATE_CHECK_ENABLED=1)
set(${PROJECT_NAME}_SOURCES ${${PROJECT_NAME}_SOURCES}
src/net/updatecheck.cpp
src/net/updatecheck.h)
message(STATUS "using update check")
else()
message(STATUS "NOT using update check")
endif()
if (MINGW)
STRING(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
if (CMAKE_BUILD_TYPE_LOWER MATCHES debug)

View File

@ -117,7 +117,7 @@ cd build
export PKG_CONFIG_PATH=/deps/lib/pkgconfig/
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=ON \
-DAPPIMAGEKIT_PACKAGE_DEBS=ON
-DAPPIMAGEKIT_PACKAGE_DEBS=ON -DUPDATE_CHECK=ON
make
make install

View File

@ -234,7 +234,7 @@ build() {
fcho "Now working in ${PWD}"
fcho "Starting cmake ..."
export CMAKE_PREFIX_PATH=$(brew --prefix qt5)
cmake -H$QTOX_DIR -B.
cmake -H$QTOX_DIR -B. -DUPDATE_CHECK=ON
make -j$(sysctl -n hw.ncpu)
}

View File

@ -104,6 +104,9 @@ void logMessageHandler(QtMsgType type, const QMessageLogContext& ctxt, const QSt
case QtDebugMsg:
LogMsg += "Debug";
break;
case QtInfoMsg:
LogMsg += "Info";
break;
case QtWarningMsg:
LogMsg += "Warning";
break;

94
src/net/updatecheck.cpp Normal file
View File

@ -0,0 +1,94 @@
/*
Copyright © 2014-2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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 3 of the License, or
(at your option) any later version.
qTox 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.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "src/net/updatecheck.h"
#include "src/persistence/settings.h"
#include <QNetworkAccessManager>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QNetworkReply>
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <cassert>
namespace {
const QString versionUrl{QStringLiteral("https://api.github.com/repos/qTox/qTox/releases/latest")};
} // namespace
UpdateCheck::UpdateCheck(const Settings& settings)
: settings(settings)
{
updateTimer.start(1000 * 60 * 60 * 24 /* 1 day */);
connect(&updateTimer, &QTimer::timeout, this, &UpdateCheck::checkForUpdate);
connect(&manager, &QNetworkAccessManager::finished, this, &UpdateCheck::handleResponse);
}
void UpdateCheck::checkForUpdate()
{
if (!settings.getCheckUpdates()) {
// still run the timer to check periodically incase setting changes
return;
}
manager.setProxy(settings.getProxy());
QNetworkRequest request{versionUrl};
manager.get(request);
}
void UpdateCheck::handleResponse(QNetworkReply *reply)
{
assert(reply != nullptr);
if (reply == nullptr) {
qWarning() << "Update check returned null reply, ignoring";
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Failed to check for update:" << reply->error();
emit updateCheckFailed();
reply->deleteLater();
return;
}
QByteArray result = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(result);
QJsonObject jObject = doc.object();
QVariantMap mainMap = jObject.toVariantMap();
QString latestVersion = mainMap["tag_name"].toString();
if (latestVersion.isEmpty()) {
qWarning() << "No tag name found in response:";
emit updateCheckFailed();
reply->deleteLater();
return;
}
// capture tag name to avoid showing update available on dev builds which include hash as part of describe
QRegularExpression versionFormat{QStringLiteral("v[0-9]+.[0-9]+.[0-9]+")};
QString curVer = versionFormat.match(GIT_DESCRIBE).captured(0);
if (latestVersion != curVer) {
qInfo() << "Update available to version" << latestVersion;
QUrl link{mainMap["html_url"].toString()};
emit updateAvailable(latestVersion, link);
}
else {
qInfo() << "qTox is up to date";
emit upToDate();
}
reply->deleteLater();
}

49
src/net/updatecheck.h Normal file
View File

@ -0,0 +1,49 @@
/*
Copyright © 2014-2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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 3 of the License, or
(at your option) any later version.
qTox 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.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QObject>
#include <QNetworkAccessManager>
#include <QTimer>
#include <memory>
class Settings;
class QString;
class QUrl;
class QNetworkReply;
class UpdateCheck : public QObject
{
Q_OBJECT
public:
UpdateCheck(const Settings& settings);
void checkForUpdate();
signals:
void updateAvailable(QString latestVersion, QUrl link);
void upToDate();
void updateCheckFailed();
private slots:
void handleResponse(QNetworkReply *reply);
private:
QNetworkAccessManager manager;
QTimer updateTimer;
const Settings& settings;
};

View File

@ -69,53 +69,55 @@ static constexpr int TYPING_NOTIFICATION_DURATION = 3000;
const QString ChatForm::ACTION_PREFIX = QStringLiteral("/me ");
QString statusToString(const Status status)
namespace
{
QString result;
switch (status) {
case Status::Online:
result = ChatForm::tr("online", "contact status");
break;
case Status::Away:
result = ChatForm::tr("away", "contact status");
break;
case Status::Busy:
result = ChatForm::tr("busy", "contact status");
break;
case Status::Offline:
result = ChatForm::tr("offline", "contact status");
break;
}
return result;
}
QString secondsToDHMS(quint32 duration)
{
QString res;
QString cD = ChatForm::tr("Call duration: ");
quint32 seconds = duration % 60;
duration /= 60;
quint32 minutes = duration % 60;
duration /= 60;
quint32 hours = duration % 24;
quint32 days = duration / 24;
// I assume no one will ever have call longer than a month
if (days) {
return cD + res.sprintf("%dd%02dh %02dm %02ds", days, hours, minutes, seconds);
QString statusToString(const Status status)
{
QString result;
switch (status) {
case Status::Online:
result = ChatForm::tr("online", "contact status");
break;
case Status::Away:
result = ChatForm::tr("away", "contact status");
break;
case Status::Busy:
result = ChatForm::tr("busy", "contact status");
break;
case Status::Offline:
result = ChatForm::tr("offline", "contact status");
break;
}
return result;
}
if (hours) {
return cD + res.sprintf("%02dh %02dm %02ds", hours, minutes, seconds);
QString secondsToDHMS(quint32 duration)
{
QString res;
QString cD = ChatForm::tr("Call duration: ");
quint32 seconds = duration % 60;
duration /= 60;
quint32 minutes = duration % 60;
duration /= 60;
quint32 hours = duration % 24;
quint32 days = duration / 24;
// I assume no one will ever have call longer than a month
if (days) {
return cD + res.sprintf("%dd%02dh %02dm %02ds", days, hours, minutes, seconds);
}
if (hours) {
return cD + res.sprintf("%02dh %02dm %02ds", hours, minutes, seconds);
}
if (minutes) {
return cD + res.sprintf("%02dm %02ds", minutes, seconds);
}
return cD + res.sprintf("%02ds", seconds);
}
if (minutes) {
return cD + res.sprintf("%02dm %02ds", minutes, seconds);
}
return cD + res.sprintf("%02ds", seconds);
}
} // namespace
ChatForm::ChatForm(Friend* chatFriend, History* history)
: GenericChatForm(chatFriend)

View File

@ -20,13 +20,29 @@
#include "aboutform.h"
#include "ui_aboutsettings.h"
#include <QDebug>
#include <QTimer>
#include "src/widget/tool/recursivesignalblocker.h"
#include "src/net/autoupdate.h"
#include "src/net/updatecheck.h"
#include "src/widget/translator.h"
#include "src/persistence/profile.h"
#include "src/persistence/settings.h"
#include <tox/tox.h>
#include "src/net/autoupdate.h"
#include "src/widget/tool/recursivesignalblocker.h"
#include "src/widget/translator.h"
#include <QDebug>
#include <QDesktopServices>
#include <QPushButton>
#include <QTimer>
#include <memory>
// index of UI in the QStackedWidget
enum class updateIndex
{
available = 0,
upToDate = 1,
failed = 2
};
/**
* @class AboutForm
@ -38,10 +54,11 @@
/**
* @brief Constructor of AboutForm.
*/
AboutForm::AboutForm()
AboutForm::AboutForm(UpdateCheck* updateCheck)
: GenericForm(QPixmap(":/img/settings/general.png"))
, bodyUI(new Ui::AboutSettings)
, progressTimer(new QTimer(this))
, updateCheck(updateCheck)
{
bodyUI->setupUi(this);
@ -84,6 +101,18 @@ void AboutForm::replaceVersions()
bodyUI->youAreUsing->setText(tr("You are using qTox version %1.").arg(QString(GIT_DESCRIBE)));
#if UPDATE_CHECK_ENABLED && !AUTOUPDATE_ENABLED
if (updateCheck != nullptr) {
connect(updateCheck, &UpdateCheck::updateAvailable, this, &AboutForm::onUpdateAvailable);
connect(updateCheck, &UpdateCheck::upToDate, this, &AboutForm::onUpToDate);
connect(updateCheck, &UpdateCheck::updateCheckFailed, this, &AboutForm::onUpdateCheckFailed);
} else {
qWarning() << "AboutForm passed null UpdateCheck!";
}
#else
qDebug() << "AboutForm not showing updates, qTox built without UPDATE_CHECK";
#endif
QString commitLink = "https://github.com/qTox/qTox/commit/" + QString(GIT_VERSION);
bodyUI->gitVersion->setText(
tr("Commit hash: %1").arg(createLink(commitLink, QString(GIT_VERSION))));
@ -146,6 +175,25 @@ void AboutForm::replaceVersions()
bodyUI->authorInfo->setText(authorInfo);
}
void AboutForm::onUpdateAvailable(QString latestVersion, QUrl link)
{
QObject::disconnect(linkConnection);
linkConnection = connect(bodyUI->updateAvailableButton, &QPushButton::clicked, [link](){
QDesktopServices::openUrl(link);
});
bodyUI->updateStack->setCurrentIndex(static_cast<int>(updateIndex::available));
}
void AboutForm::onUpToDate()
{
bodyUI->updateStack->setCurrentIndex(static_cast<int>(updateIndex::upToDate));
}
void AboutForm::onUpdateCheckFailed()
{
bodyUI->updateStack->setCurrentIndex(static_cast<int>(updateIndex::failed));
}
/**
* @brief Creates hyperlink with specific style.
* @param path The URL of the page the link goes to.

View File

@ -22,9 +22,12 @@
#include "genericsettings.h"
#include <memory>
class Core;
class QTimer;
class QString;
class UpdateCheck;
class QLayoutItem;
namespace Ui {
class AboutSettings;
@ -34,13 +37,18 @@ class AboutForm : public GenericForm
{
Q_OBJECT
public:
AboutForm();
AboutForm(UpdateCheck* updateCheck);
~AboutForm();
virtual QString getFormName() final override
{
return tr("About");
}
public slots:
void onUpdateAvailable(QString latestVersion, QUrl link);
void onUpToDate();
void onUpdateCheckFailed();
protected:
private slots:
void showUpdateProgress();
@ -55,6 +63,8 @@ private:
private:
Ui::AboutSettings* bodyUI;
QTimer* progressTimer;
UpdateCheck* updateCheck;
QMetaObject::Connection linkConnection;
};
#endif // ABOUTFORM_H

View File

@ -175,6 +175,89 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QStackedWidget" name="updateStack">
<property name="currentIndex">
<number>2</number>
</property>
<widget class="QWidget" name="updateAvailablePage">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<layout class="QHBoxLayout" name="updateAvailableLayout">
<item>
<spacer name="updateAvailableSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="updateAvailableButton">
<property name="accessibleDescription">
<string>Open update download link</string>
</property>
<property name="text">
<string>Update available</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="upToDatePage">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<layout class="QHBoxLayout" name="upToDateLayout">
<item>
<spacer name="upToDateSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="upToDateLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>qTox is up to date ✓</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="blankPage">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="blankPageFiller">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -100,9 +100,7 @@ GeneralForm::GeneralForm(SettingsWidget* myParent)
Settings& s = Settings::getInstance();
#ifdef AUTOUPDATE_ENABLED
bodyUI->checkUpdates->setVisible(AUTOUPDATE_ENABLED);
#else
#if !defined(AUTOUPDATE_ENABLED) && !defined(UPDATE_CHECK_ENABLED)
bodyUI->checkUpdates->setVisible(false);
#endif

View File

@ -111,7 +111,7 @@
<item>
<widget class="QCheckBox" name="checkUpdates">
<property name="text">
<string>Check for updates on startup</string>
<string>Check for updates</string>
</property>
</widget>
</item>

View File

@ -22,6 +22,7 @@
#include "src/audio/audio.h"
#include "src/core/coreav.h"
#include "src/core/core.h"
#include "src/net/updatecheck.h"
#include "src/persistence/settings.h"
#include "src/video/camerasource.h"
#include "src/widget/contentlayout.h"
@ -40,7 +41,7 @@
#include <memory>
SettingsWidget::SettingsWidget(QWidget* parent)
SettingsWidget::SettingsWidget(UpdateCheck* updateCheck, QWidget* parent)
: QWidget(parent, Qt::Window)
{
Audio* audio = &Audio::getInstance();
@ -63,7 +64,15 @@ SettingsWidget::SettingsWidget(QWidget* parent)
AVForm* rawAvfrm = new AVForm(audio, coreAV, camera, audioSettings, videoSettings);
std::unique_ptr<AVForm> avfrm(rawAvfrm);
std::unique_ptr<AdvancedForm> expfrm(new AdvancedForm());
std::unique_ptr<AboutForm> abtfrm(new AboutForm());
std::unique_ptr<AboutForm> abtfrm(new AboutForm(updateCheck));
#if UPDATE_CHECK_ENABLED && !AUTOUPDATE_ENABLED
if (updateCheck != nullptr) {
connect(updateCheck, &UpdateCheck::updateAvailable, this, &SettingsWidget::onUpdateAvailable);
} else {
qWarning() << "SettingsWidget passed null UpdateCheck!";
}
#endif
cfgForms = {{std::move(gfrm), std::move(uifrm), std::move(pfrm), std::move(avfrm), std::move(expfrm), std::move(abtfrm)}};
for (auto& cfgForm : cfgForms)
@ -111,6 +120,13 @@ void SettingsWidget::onTabChanged(int index)
settingsWidgets->setCurrentIndex(index);
}
void SettingsWidget::onUpdateAvailable(void)
{
settingsWidgets->tabBar()->setProperty("update-available", true);
settingsWidgets->tabBar()->style()->unpolish(settingsWidgets->tabBar());
settingsWidgets->tabBar()->style()->polish(settingsWidgets->tabBar());
}
void SettingsWidget::retranslateUi()
{
for (size_t i = 0; i < cfgForms.size(); ++i)

View File

@ -35,12 +35,13 @@ class AVForm;
class QLabel;
class QTabWidget;
class ContentLayout;
class UpdateCheck;
class SettingsWidget : public QWidget
{
Q_OBJECT
public:
explicit SettingsWidget(QWidget* parent = nullptr);
SettingsWidget(UpdateCheck* updateCheck, QWidget* parent = nullptr);
~SettingsWidget();
bool isShown() const;
@ -49,6 +50,9 @@ public:
void showAbout();
public slots:
void onUpdateAvailable(void);
private slots:
void onTabChanged(int);

View File

@ -61,6 +61,7 @@
#include "src/model/groupinvite.h"
#include "src/model/profile/profileinfo.h"
#include "src/net/autoupdate.h"
#include "src/net/updatecheck.h"
#include "src/nexus.h"
#include "src/persistence/offlinemsgengine.h"
#include "src/persistence/profile.h"
@ -232,6 +233,14 @@ void Widget::init()
filesForm = new FilesForm();
addFriendForm = new AddFriendForm;
groupInviteForm = new GroupInviteForm;
#if UPDATE_CHECK_ENABLED
updateCheck = std::unique_ptr<UpdateCheck>(new UpdateCheck(Settings::getInstance()));
connect(updateCheck.get(), &UpdateCheck::updateAvailable, this, &Widget::onUpdateAvailable);
#endif
settingsWidget = new SettingsWidget(updateCheck.get(), this);
#if UPDATE_CHECK_ENABLED
updateCheck->checkForUpdate();
#endif
Core* core = Nexus::getCore();
Profile* profile = Nexus::getProfile();
@ -848,10 +857,6 @@ void Widget::onIconClick(QSystemTrayIcon::ActivationReason reason)
void Widget::onShowSettings()
{
if (!settingsWidget) {
settingsWidget = new SettingsWidget(this);
}
if (Settings::getInstance().getSeparateWindow()) {
if (!settingsWidget->isShown()) {
settingsWidget->show(createContentDialog(DialogType::SettingDialog));
@ -1589,6 +1594,13 @@ void Widget::toggleFullscreen()
}
}
void Widget::onUpdateAvailable(QString /*latestVersion*/, QUrl /*link*/)
{
ui->settingsButton->setProperty("update-available", true);
ui->settingsButton->style()->unpolish(ui->settingsButton);
ui->settingsButton->style()->polish(ui->settingsButton);
}
ContentDialog* Widget::createContentDialog() const
{
ContentDialog* contentDialog = new ContentDialog();

View File

@ -69,6 +69,7 @@ class QTimer;
class SettingsWidget;
class SystemTrayIcon;
class VideoSurface;
class UpdateCheck;
class Widget final : public QMainWindow
{
@ -183,6 +184,7 @@ public slots:
void onGroupDialogShown(Group* g);
void toggleFullscreen();
void refreshPeerListsLocal(const QString &username);
void onUpdateAvailable(QString latestVersion, QUrl link);
signals:
void friendRequestAccepted(const ToxPk& friendPk);
@ -288,6 +290,7 @@ private:
ProfileForm* profileForm;
QPointer<SettingsWidget> settingsWidget;
std::unique_ptr<UpdateCheck> updateCheck; // ownership should be moved outside Widget once non-singleton
FilesForm* filesForm;
static Widget* instance;
GenericChatroomWidget* activeChatroomWidget;

View File

@ -105,6 +105,17 @@ QScrollBar:vertical
margin-bottom: 2px;
}
/* using last is a bit of a hack, but QTabBar otherwise doesn't allow selecting single tabs */
QTabBar::tab:last:!selected[update-available=true]
{
background-color: #80c580;
}
QPushButton#updateAvailableButton
{
background-color: #21da21;
}
QScrollBar::handle:vertical
{
background-color: #d1d1d1;

View File

@ -1,3 +1,15 @@
QPushButton[update-available=true]
{
background-color: #115508;
border: none;
}
QPushButton:hover[update-available=true]
{
background-color: #2b9e1c;
border: none;
}
QPushButton
{
background-color: @themeDark;

View File

@ -1130,12 +1130,14 @@ then
cmake -DCMAKE_TOOLCHAIN_FILE=./toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DSPELL_CHECK=OFF \
-DUPDATE_CHECK=ON \
..
elif [[ "$BUILD_TYPE" == "debug" ]]
then
cmake -DCMAKE_TOOLCHAIN_FILE=./toolchain.cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DSPELL_CHECK=OFF \
-DUPDATE_CHECK=ON \
..
fi