diff --git a/src/gui/feedmessageviewer.cpp b/src/gui/feedmessageviewer.cpp index 092791f12..fe5a50af0 100755 --- a/src/gui/feedmessageviewer.cpp +++ b/src/gui/feedmessageviewer.cpp @@ -57,7 +57,6 @@ #include #include #include -#include #include #include #include diff --git a/src/main.cpp b/src/main.cpp index 410a20754..2192ff795 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -108,9 +108,6 @@ int main(int argc, char* argv[]) { Application::setOrganizationDomain(APP_URL); Application::setWindowIcon(QIcon(APP_ICON_PATH)); - // Load activated accounts. - qApp->feedReader()->feedsModel()->loadActivatedServiceAccounts(); - // Setup single-instance behavior. QObject::connect(&application, &Application::messageReceived, &application, &Application::processExecutionMessage); qDebug().nospace() << "Creating main application form in thread: \'" << QThread::currentThreadId() << "\'."; @@ -139,6 +136,9 @@ int main(int argc, char* argv[]) { qApp->showTrayIcon(); } + // Load activated accounts. + qApp->feedReader()->feedsModel()->loadActivatedServiceAccounts(); + if (qApp->isFirstRun() || qApp->isFirstRun(APP_VERSION)) { qApp->showGuiMessage(QSL(APP_NAME), QObject::tr("Welcome to %1.\n\nPlease, check NEW stuff included in this\n" "version by clicking this popup notification.").arg(APP_LONG_NAME), diff --git a/src/network-web/oauth2service.cpp b/src/network-web/oauth2service.cpp index 8494dbf12..21827cd00 100755 --- a/src/network-web/oauth2service.cpp +++ b/src/network-web/oauth2service.cpp @@ -69,12 +69,26 @@ OAuth2Service::OAuth2Service(QString authUrl, QString tokenUrl, QString clientId } QString OAuth2Service::bearer() { - if (login()) { - return QString("Bearer %1").arg(m_accessToken); - } - else { + if (!isFullyLoggedIn()) { + qApp->showGuiMessage(tr("Inoreader: you have to login first"), + tr("Click here to login."), + QSystemTrayIcon::Critical, + nullptr, false, + [this]() { + login(); + }); return QString(); } + else { + return QString("Bearer %1").arg(m_accessToken); + } +} + +bool OAuth2Service::isFullyLoggedIn() const { + bool is_expiration_valid = m_tokensExpireIn > QDateTime::currentDateTime(); + bool do_tokens_exist = !m_refreshToken.isEmpty() && !m_accessToken.isEmpty(); + + return is_expiration_valid && do_tokens_exist; } void OAuth2Service::setOAuthTokenGrantType(QString grant_type) { @@ -124,15 +138,15 @@ void OAuth2Service::refreshAccessToken(QString refresh_token) { .arg(refresh_token) .arg("refresh_token"); + qApp->showGuiMessage(tr("Logging in via OAuth 2.0..."), + tr("Refreshing login tokens for '%1'...").arg(m_tokenUrl.toString()), + QSystemTrayIcon::MessageIcon::Information); + m_networkManager.post(networkRequest, content.toUtf8()); } -void OAuth2Service::cleanTokens() { - m_refreshToken = m_accessToken = QString(); -} - -void OAuth2Service::tokenRequestFinished(QNetworkReply* networkReply) { - QJsonDocument jsonDocument = QJsonDocument::fromJson(networkReply->readAll()); +void OAuth2Service::tokenRequestFinished(QNetworkReply* network_reply) { + QJsonDocument jsonDocument = QJsonDocument::fromJson(network_reply->readAll()); QJsonObject rootObject = jsonDocument.object(); qDebug() << "Token response:"; @@ -142,8 +156,7 @@ void OAuth2Service::tokenRequestFinished(QNetworkReply* networkReply) { QString error = rootObject.value("error").toString(); QString error_description = rootObject.value("error_description").toString(); - cleanTokens(); - login(); + logout(); emit tokensRetrieveError(error, error_description); } @@ -160,7 +173,7 @@ void OAuth2Service::tokenRequestFinished(QNetworkReply* networkReply) { emit tokensReceived(m_accessToken, m_refreshToken, rootObject.value("expires_in").toInt()); } - networkReply->deleteLater(); + network_reply->deleteLater(); } QString OAuth2Service::accessToken() const { @@ -248,8 +261,13 @@ void OAuth2Service::retrieveAuthCode() { connect(&login_page, &OAuthLogin::authGranted, this, &OAuth2Service::authCodeObtained); connect(&login_page, &OAuthLogin::authRejected, [this]() { - cleanTokens(); + logout(); emit authFailed(); }); + + qApp->showGuiMessage(tr("Logging in via OAuth 2.0..."), + tr("Requesting access authorization for '%1'...").arg(m_authUrl), + QSystemTrayIcon::MessageIcon::Information); + login_page.login(auth_url, m_redirectUrl); } diff --git a/src/network-web/oauth2service.h b/src/network-web/oauth2service.h index cd27ce41d..59335d501 100755 --- a/src/network-web/oauth2service.h +++ b/src/network-web/oauth2service.h @@ -52,7 +52,13 @@ class OAuth2Service : public QObject { explicit OAuth2Service(QString authUrl, QString tokenUrl, QString clientId, QString clientSecret, QString scope, QObject* parent = 0); + // Returns bearer HTTP header value. + // NOTE: Only call this if isFullyLoggedIn() + // returns true. If isFullyLoggedIn() returns + // false, then you must call login() on + // main GUI thread. QString bearer(); + bool isFullyLoggedIn() const; void setOAuthTokenGrantType(QString grant_type); QString oAuthTokenGrantType(); @@ -95,12 +101,14 @@ class OAuth2Service : public QObject { // access token is made. // Returns true, if user is already logged in (final state). // Returns false, if user is NOT logged in (asynchronous flow). + // + // NOTE: This can be called ONLY on main GUI thread, + // because widgets may be displayed. bool login(); void logout(); private slots: - void cleanTokens(); - void tokenRequestFinished(QNetworkReply* networkReply); + void tokenRequestFinished(QNetworkReply* network_reply); private: QDateTime m_tokensExpireIn; diff --git a/src/services/abstract/feed.cpp b/src/services/abstract/feed.cpp index 29a073199..c9e7d4c10 100755 --- a/src/services/abstract/feed.cpp +++ b/src/services/abstract/feed.cpp @@ -81,19 +81,6 @@ QList Feed::undeletedMessages() const { QVariant Feed::data(int column, int role) const { switch (role) { - case Qt::ToolTipRole: - if (column == FDS_MODEL_TITLE_INDEX) { - //: Tooltip for feed. - return tr("%1" - "%2\n\n" - "Auto-update status: %3").arg(title(), - description().isEmpty() ? QString() : QString('\n') + description(), - getAutoUpdateStatusDescription()); - } - else { - return RootItem::data(column, role); - } - case Qt::ForegroundRole: switch (status()) { case NewMessages: @@ -308,3 +295,27 @@ QString Feed::getAutoUpdateStatusDescription() const { return auto_update_string; } + +QString Feed::getStatusDescription() const { + switch (m_status) { + case Status::Normal: + return tr("no errors"); + + case Status::NewMessages: + return tr("has new messages"); + + case Status::AuthError: + return tr("authentication error"); + + case Status::NetworkError: + return tr("network error"); + + default: + return tr("unspecified error"); + } +} + +QString Feed::additionalTooltip() const { + return tr("Auto-update status: %1\n" + "Status: %2").arg(getAutoUpdateStatusDescription(), getStatusDescription()); +} diff --git a/src/services/abstract/feed.h b/src/services/abstract/feed.h index 1eefb7e5f..235b17653 100755 --- a/src/services/abstract/feed.h +++ b/src/services/abstract/feed.h @@ -46,8 +46,9 @@ class Feed : public RootItem, public QRunnable { Normal = 0, NewMessages = 1, NetworkError = 2, - ParsingError = 3, - OtherError = 4 + AuthError = 3, + ParsingError = 4, + OtherError = 5 }; // Constructors. @@ -58,6 +59,8 @@ class Feed : public RootItem, public QRunnable { QList undeletedMessages() const; + QString additionalTooltip() const; + int countOfAllMessages() const; int countOfUnreadMessages() const; @@ -93,6 +96,7 @@ class Feed : public RootItem, public QRunnable { protected: QString getAutoUpdateStatusDescription() const; + QString getStatusDescription() const; signals: void messagesObtained(QList messages, bool error_during_obtaining); diff --git a/src/services/abstract/recyclebin.cpp b/src/services/abstract/recyclebin.cpp index 5a5fb1fab..26dac8612 100755 --- a/src/services/abstract/recyclebin.cpp +++ b/src/services/abstract/recyclebin.cpp @@ -39,6 +39,10 @@ RecycleBin::RecycleBin(RootItem* parent_item) : RootItem(parent_item), m_totalCo RecycleBin::~RecycleBin() {} +QString RecycleBin::additionalTooltip() const { + return tr("%n deleted message(s).", 0, countOfAllMessages()); +} + int RecycleBin::countOfUnreadMessages() const { return m_unreadCount; } @@ -60,16 +64,6 @@ void RecycleBin::updateCounts(bool update_total_count) { } } -QVariant RecycleBin::data(int column, int role) const { - switch (role) { - case Qt::ToolTipRole: - return tr("Recycle bin\n\n%1").arg(tr("%n deleted message(s).", 0, countOfAllMessages())); - - default: - return RootItem::data(column, role); - } -} - QList RecycleBin::contextMenu() { if (m_contextMenu.isEmpty()) { QAction* restore_action = new QAction(qApp->icons()->fromTheme(QSL("view-refresh")), diff --git a/src/services/abstract/recyclebin.h b/src/services/abstract/recyclebin.h index 1798ab740..56186ddca 100755 --- a/src/services/abstract/recyclebin.h +++ b/src/services/abstract/recyclebin.h @@ -28,7 +28,7 @@ class RecycleBin : public RootItem { explicit RecycleBin(RootItem* parent_item = nullptr); virtual ~RecycleBin(); - QVariant data(int column, int role) const; + QString additionalTooltip() const; QList contextMenu(); QList undeletedMessages() const; diff --git a/src/services/abstract/rootitem.cpp b/src/services/abstract/rootitem.cpp index 841cc5704..985928ac1 100755 --- a/src/services/abstract/rootitem.cpp +++ b/src/services/abstract/rootitem.cpp @@ -58,6 +58,10 @@ QString RootItem::hashCode() const { QString::number(id()); } +QString RootItem::additionalTooltip() const { + return QString(); +} + QList RootItem::contextMenu() { return QList(); } @@ -142,7 +146,13 @@ QVariant RootItem::data(int column, int role) const { QString tool_tip = m_title; if (!m_description.isEmpty()) { - tool_tip += QL1S("\n\n") + m_description; + tool_tip += QL1S("\n") + m_description; + } + + QString extra_tooltip = additionalTooltip(); + + if (!extra_tooltip.isEmpty()) { + tool_tip += QL1S("\n\n") + extra_tooltip; } return tool_tip; diff --git a/src/services/abstract/rootitem.h b/src/services/abstract/rootitem.h index be64a0580..53194efcc 100755 --- a/src/services/abstract/rootitem.h +++ b/src/services/abstract/rootitem.h @@ -71,6 +71,7 @@ class RootItem : public QObject { virtual ~RootItem(); virtual QString hashCode() const; + virtual QString additionalTooltip() const; // Returns list of specific actions which can be done with the item. // Do not include general actions here like actions: Mark as read, Update, ... diff --git a/src/services/inoreader/inoreaderfeed.cpp b/src/services/inoreader/inoreaderfeed.cpp index 7d4e2c232..2c5e38f18 100755 --- a/src/services/inoreader/inoreaderfeed.cpp +++ b/src/services/inoreader/inoreaderfeed.cpp @@ -32,7 +32,14 @@ InoreaderServiceRoot* InoreaderFeed::serviceRoot() const { } QList InoreaderFeed::obtainNewMessages(bool* error_during_obtaining) { - QList messages = serviceRoot()->network()->messages(customId(), error_during_obtaining); + Feed::Status error; + QList messages = serviceRoot()->network()->messages(customId(), error); + + setStatus(error); + + if (error == Feed::Status::NetworkError || error == Feed::Status::AuthError) { + *error_during_obtaining = true; + } return messages; } diff --git a/src/services/inoreader/inoreaderserviceroot.cpp b/src/services/inoreader/inoreaderserviceroot.cpp index ce671783e..b446dad5c 100755 --- a/src/services/inoreader/inoreaderserviceroot.cpp +++ b/src/services/inoreader/inoreaderserviceroot.cpp @@ -120,9 +120,10 @@ void InoreaderServiceRoot::start(bool freshly_activated) { Q_UNUSED(freshly_activated) loadFromDatabase(); - m_network->oauth()->login(); loadCacheFromFile(accountId()); + m_network->oauth()->login(); + if (childCount() <= 1) { syncIn(); } diff --git a/src/services/inoreader/network/inoreadernetworkfactory.cpp b/src/services/inoreader/network/inoreadernetworkfactory.cpp index 952cec0f9..3498c1688 100755 --- a/src/services/inoreader/network/inoreadernetworkfactory.cpp +++ b/src/services/inoreader/network/inoreadernetworkfactory.cpp @@ -66,17 +66,18 @@ void InoreaderNetworkFactory::setBatchSize(int batch_size) { } void InoreaderNetworkFactory::initializeOauth() { - connect(m_oauth2, &OAuth2Service::tokensRetrieveError, [](QString error, QString error_description) { - Q_UNUSED(error) - - qApp->showGuiMessage("Authentication error - Inoreader", error_description, QSystemTrayIcon::Critical); - }); + connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &InoreaderNetworkFactory::onTokensError); + connect(m_oauth2, &OAuth2Service::authFailed, this, &InoreaderNetworkFactory::onAuthFailed); connect(m_oauth2, &OAuth2Service::tokensReceived, [this](QString access_token, QString refresh_token, int expires_in) { Q_UNUSED(expires_in) if (m_service != nullptr && !access_token.isEmpty() && !refresh_token.isEmpty()) { QSqlDatabase database = qApp->database()->connection(metaObject()->className(), DatabaseFactory::FromSettings); DatabaseQueries::storeNewInoreaderTokens(database, refresh_token, m_service->accountId()); + + qApp->showGuiMessage(tr("Logged in successfully"), + tr("Your login to Inoreader was authorized."), + QSystemTrayIcon::MessageIcon::Information); } }); } @@ -121,13 +122,14 @@ RootItem* InoreaderNetworkFactory::feedsCategories(bool obtain_icons) { return decodeFeedCategoriesData(category_data, feed_data, obtain_icons); } -QList InoreaderNetworkFactory::messages(const QString& stream_id, bool* is_error) { +QList InoreaderNetworkFactory::messages(const QString& stream_id, Feed::Status& error) { Downloader downloader; QEventLoop loop; QString target_url = INOREADER_API_FEED_CONTENTS; QString bearer = m_oauth2->bearer().toLocal8Bit(); if (bearer.isEmpty()) { + error == Feed::Status::AuthError; return QList(); } @@ -140,12 +142,13 @@ QList InoreaderNetworkFactory::messages(const QString& stream_id, bool* loop.exec(); if (downloader.lastOutputError() != QNetworkReply::NetworkError::NoError) { - *is_error = true; + error == Feed::Status::NetworkError; return QList(); } else { QString messages_data = downloader.lastOutputData(); + error == Feed::Status::Normal; return decodeMessages(messages_data, stream_id); } } @@ -204,6 +207,28 @@ void InoreaderNetworkFactory::markMessagesRead(RootItem::ReadStatus status, cons void InoreaderNetworkFactory::markMessagesStarred(RootItem::Importance importance, const QStringList& custom_ids) {} +void InoreaderNetworkFactory::onTokensError(const QString& error, const QString& error_description) { + Q_UNUSED(error) + + qApp->showGuiMessage(tr("Inoreader: authentication error"), + tr("Click this to login again. Error is: '%1'").arg(error_description), + QSystemTrayIcon::Critical, + nullptr, false, + [this]() { + m_oauth2->login(); + }); +} + +void InoreaderNetworkFactory::onAuthFailed() { + qApp->showGuiMessage(tr("Inoreader: authorization denied"), + tr("Click this to login again."), + QSystemTrayIcon::Critical, + nullptr, false, + [this]() { + m_oauth2->login(); + }); +} + QList InoreaderNetworkFactory::decodeMessages(const QString& messages_json_data, const QString& stream_id) { QList messages; QJsonArray json = QJsonDocument::fromJson(messages_json_data.toUtf8()).object()["items"].toArray(); diff --git a/src/services/inoreader/network/inoreadernetworkfactory.h b/src/services/inoreader/network/inoreadernetworkfactory.h index 38f7971ad..fcf53492c 100755 --- a/src/services/inoreader/network/inoreadernetworkfactory.h +++ b/src/services/inoreader/network/inoreadernetworkfactory.h @@ -23,6 +23,7 @@ #include "core/message.h" +#include "services/abstract/feed.h" #include "services/abstract/rootitem.h" #include @@ -53,10 +54,14 @@ class InoreaderNetworkFactory : public QObject { // Returned items do not have primary IDs assigned. RootItem* feedsCategories(bool obtain_icons); - QList messages(const QString& stream_id, bool* is_error); + QList messages(const QString& stream_id, Feed::Status& error); void markMessagesRead(RootItem::ReadStatus status, const QStringList& custom_ids); void markMessagesStarred(RootItem::Importance importance, const QStringList& custom_ids); + private slots: + void onTokensError(const QString& error, const QString& error_description); + void onAuthFailed(); + private: QList decodeMessages(const QString& messages_json_data, const QString& stream_id); RootItem* decodeFeedCategoriesData(const QString& categories, const QString& feeds, bool obtain_icons); diff --git a/src/services/standard/standardfeed.cpp b/src/services/standard/standardfeed.cpp index 344f17a1a..989d569a2 100755 --- a/src/services/standard/standardfeed.cpp +++ b/src/services/standard/standardfeed.cpp @@ -71,6 +71,14 @@ QList StandardFeed::contextMenu() { return serviceRoot()->getContextMenuForFeed(this); } +QString StandardFeed::additionalTooltip() const { + return Feed::additionalTooltip() + tr("\nNetwork status: %1\n" + "Encoding: %2\n" + "Type: %3").arg(NetworkFactory::networkErrorText(m_networkError), + encoding(), + StandardFeed::typeToString(type())); +} + bool StandardFeed::canBeEdited() const { return true; } @@ -99,31 +107,6 @@ bool StandardFeed::deleteViaGui() { } } -QVariant StandardFeed::data(int column, int role) const { - switch (role) { - case Qt::ToolTipRole: - if (column == FDS_MODEL_TITLE_INDEX) { - //: Tooltip for feed. - return tr("%1 (%2)" - "%3\n\n" - "Network status: %6\n" - "Encoding: %4\n" - "Auto-update status: %5").arg(title(), - StandardFeed::typeToString(type()), - description().isEmpty() ? QString() : QString('\n') + description(), - encoding(), - getAutoUpdateStatusDescription(), - NetworkFactory::networkErrorText(m_networkError)); - } - else { - return Feed::data(column, role); - } - - default: - return Feed::data(column, role); - } -} - QString StandardFeed::typeToString(StandardFeed::Type type) { switch (type) { case Atom10: @@ -452,7 +435,6 @@ QList StandardFeed::obtainNewMessages(bool* error_during_obtaining) { return QList(); } else if (status() != NewMessages) { - setStatus(Normal); *error_during_obtaining = false; } diff --git a/src/services/standard/standardfeed.h b/src/services/standard/standardfeed.h index a8655a002..a05d23f6b 100755 --- a/src/services/standard/standardfeed.h +++ b/src/services/standard/standardfeed.h @@ -53,14 +53,14 @@ class StandardFeed : public Feed { QList contextMenu(); + QString additionalTooltip() const; + bool canBeEdited() const; bool canBeDeleted() const; bool editViaGui(); bool deleteViaGui(); - QVariant data(int column, int role) const; - // Obtains data related to this feed. Qt::ItemFlags additionalFlags() const; bool performDragDropChange(RootItem* target_item); diff --git a/src/services/standard/standardserviceroot.cpp b/src/services/standard/standardserviceroot.cpp index bd1cd5b1f..c8901d61c 100755 --- a/src/services/standard/standardserviceroot.cpp +++ b/src/services/standard/standardserviceroot.cpp @@ -141,21 +141,6 @@ void StandardServiceRoot::addNewFeed(const QString& url) { qApp->feedUpdateLock()->unlock(); } -QVariant StandardServiceRoot::data(int column, int role) const { - switch (role) { - case Qt::ToolTipRole: - if (column == FDS_MODEL_TITLE_INDEX) { - return tr("This is service account for standard RSS/RDF/ATOM feeds.\n\nAccount ID: %1").arg(accountId()); - } - else { - return ServiceRoot::data(column, role); - } - - default: - return ServiceRoot::data(column, role); - } -} - Qt::ItemFlags StandardServiceRoot::additionalFlags() const { return Qt::ItemIsDropEnabled; } diff --git a/src/services/standard/standardserviceroot.h b/src/services/standard/standardserviceroot.h index 60491cd8a..702e8cda7 100755 --- a/src/services/standard/standardserviceroot.h +++ b/src/services/standard/standardserviceroot.h @@ -48,7 +48,6 @@ class StandardServiceRoot : public ServiceRoot { bool supportsFeedAdding() const; bool supportsCategoryAdding() const; - QVariant data(int column, int role) const; Qt::ItemFlags additionalFlags() const; // Returns menu to be shown in "Services -> service" menu. diff --git a/src/services/tt-rss/ttrssserviceroot.cpp b/src/services/tt-rss/ttrssserviceroot.cpp index fdd743025..2a6013e4a 100755 --- a/src/services/tt-rss/ttrssserviceroot.cpp +++ b/src/services/tt-rss/ttrssserviceroot.cpp @@ -125,28 +125,6 @@ bool TtRssServiceRoot::canBeDeleted() const { return true; } -QVariant TtRssServiceRoot::data(int column, int role) const { - switch (role) { - case Qt::ToolTipRole: - if (column == FDS_MODEL_TITLE_INDEX) { - return tr("Tiny Tiny RSS\n\nAccount ID: %3\nUsername: %1\nServer: %2\n" - "Last error: %4\nLast login on: %5").arg(m_network->username(), - m_network->url(), - QString::number(accountId()), - NetworkFactory::networkErrorText(m_network->lastError()), - m_network->lastLoginTime().isValid() ? - m_network->lastLoginTime().toString(Qt::DefaultLocaleShortDate) : - QSL("-")); - } - else { - return ServiceRoot::data(column, role); - } - - default: - return ServiceRoot::data(column, role); - } -} - void TtRssServiceRoot::saveAllCachedData() { QPair, QMap>> msgCache = takeMessageCache(); QMapIterator i(msgCache.first); @@ -193,6 +171,16 @@ QList TtRssServiceRoot::serviceMenu() { return m_serviceMenu; } +QString TtRssServiceRoot::additionalTooltip() const { + return tr("Username: %1\nServer: %2\n" + "Last error: %3\nLast login on: %4").arg(m_network->username(), + m_network->url(), + NetworkFactory::networkErrorText(m_network->lastError()), + m_network->lastLoginTime().isValid() ? + m_network->lastLoginTime().toString(Qt::DefaultLocaleShortDate) : + QSL("-")); +} + TtRssNetworkFactory* TtRssServiceRoot::network() const { return m_network; } diff --git a/src/services/tt-rss/ttrssserviceroot.h b/src/services/tt-rss/ttrssserviceroot.h index 3b3a1476e..b1d3f526d 100755 --- a/src/services/tt-rss/ttrssserviceroot.h +++ b/src/services/tt-rss/ttrssserviceroot.h @@ -44,9 +44,10 @@ class TtRssServiceRoot : public ServiceRoot, public CacheForServiceRoot { bool deleteViaGui(); bool supportsFeedAdding() const; bool supportsCategoryAdding() const; - QVariant data(int column, int role) const; QList serviceMenu(); + QString additionalTooltip() const; + void saveAllCachedData(); // Access to network.