diff --git a/forms/accountpopupform.ui b/forms/accountpopupform.ui index 969e73e8e..d0467ad9f 100644 --- a/forms/accountpopupform.ui +++ b/forms/accountpopupform.ui @@ -6,7 +6,7 @@ 0 0 - 461 + 469 301 @@ -189,6 +189,9 @@ + + false + Follow @@ -196,6 +199,9 @@ + + true + Ignore diff --git a/src/singletons/accountmanager.hpp b/src/singletons/accountmanager.hpp index b80d79a8a..d72424295 100644 --- a/src/singletons/accountmanager.hpp +++ b/src/singletons/accountmanager.hpp @@ -17,5 +17,5 @@ public: twitch::TwitchAccountManager Twitch; }; +} // namespace singletons } // namespace chatterino -} diff --git a/src/twitch/twitchaccountmanager.cpp b/src/twitch/twitchaccountmanager.cpp index 70ad8d0a7..f5bb57ddc 100644 --- a/src/twitch/twitchaccountmanager.cpp +++ b/src/twitch/twitchaccountmanager.cpp @@ -186,5 +186,6 @@ TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser( return AddUserResponse::UserAdded; } -} -} + +} // namespace twitch +} // namespace chatterino diff --git a/src/twitch/twitchaccountmanager.hpp b/src/twitch/twitchaccountmanager.hpp index 191a86b40..535f3e1c2 100644 --- a/src/twitch/twitchaccountmanager.hpp +++ b/src/twitch/twitchaccountmanager.hpp @@ -63,5 +63,6 @@ private: friend class chatterino::singletons::AccountManager; }; -} -} + +} // namespace twitch +} // namespace chatterino diff --git a/src/util/networkmanager.hpp b/src/util/networkmanager.hpp index 5ea0a3b3a..dc43c44ff 100644 --- a/src/util/networkmanager.hpp +++ b/src/util/networkmanager.hpp @@ -159,6 +159,28 @@ public: emit requester.requestUrl(); } + + template + static void urlDelete(QNetworkRequest request, FinishedCallback onFinished) + { + NetworkRequester requester; + NetworkWorker *worker = new NetworkWorker; + + worker->moveToThread(&NetworkManager::workerThread); + QObject::connect( + &requester, &NetworkRequester::requestUrl, worker, + [ onFinished = std::move(onFinished), request = std::move(request), worker ]() { + QNetworkReply *reply = NetworkManager::NaM.deleteResource(request); + + QObject::connect(reply, &QNetworkReply::finished, + [ onFinished = std::move(onFinished), reply, worker ]() { + onFinished(reply); + delete worker; + }); + }); + + emit requester.requestUrl(); + } }; class NetworkRequest diff --git a/src/util/urlfetch.hpp b/src/util/urlfetch.hpp index f2bb884bd..a9b180e85 100644 --- a/src/util/urlfetch.hpp +++ b/src/util/urlfetch.hpp @@ -1,8 +1,8 @@ #pragma once -#include "singletons/accountmanager.hpp" #include "credentials.hpp" #include "debug/log.hpp" +#include "singletons/accountmanager.hpp" #include "util/networkmanager.hpp" #include @@ -160,6 +160,36 @@ static void put(QUrl url, std::function successCallback) }); } +static void sendDelete(QUrl url, std::function successCallback) +{ + QNetworkRequest request(url); + + auto &accountManager = singletons::AccountManager::getInstance(); + auto currentTwitchUser = accountManager.Twitch.getCurrent(); + QByteArray oauthToken; + if (currentTwitchUser) { + oauthToken = currentTwitchUser->getOAuthToken().toUtf8(); + } else { + // XXX(pajlada): Bail out? + } + + request.setRawHeader("Client-ID", getDefaultClientID()); + request.setRawHeader("Accept", "application/vnd.twitchtv.v5+json"); + request.setRawHeader("Authorization", "OAuth " + oauthToken); + + NetworkManager::urlDelete(std::move(request), [=](QNetworkReply *reply) { + if (reply->error() == QNetworkReply::NetworkError::NoError) { + int code = + reply->attribute(QNetworkRequest::Attribute::HttpStatusCodeAttribute).toInt(); + + if (code >= 200 && code <= 299) { + successCallback(); + } + } + reply->deleteLater(); + }); +} + } // namespace twitch } // namespace util } // namespace chatterino diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index d99fe3955..78020a8cf 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -31,9 +31,29 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) this->resize(0, 0); + auto &accountManager = singletons::AccountManager::getInstance(); + + connect(this, &AccountPopupWidget::refreshButtons, this, + &AccountPopupWidget::actuallyRefreshButtons, Qt::QueuedConnection); + + accountManager.Twitch.userChanged.connect([this] { + singletons::AccountManager &accountManager = singletons::AccountManager::getInstance(); + + auto currentTwitchUser = accountManager.Twitch.getCurrent(); + if (!currentTwitchUser) { + // No twitch user set (should never happen) + return; + } + + this->loggedInUser.username = currentTwitchUser->getUserName(); + this->loggedInUser.userID = currentTwitchUser->getUserId(); + + this->loggedInUser.refreshUserType(this->channel, true); + + }); + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); - this->permission = permissions::User; for (auto button : this->ui->profileLayout->findChildren()) { button->setFocusProxy(this); } @@ -58,17 +78,8 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) this->sendCommand(this->ui->mod, "/mod "); this->sendCommand(this->ui->unMod, "/unmod "); - auto &accountManager = singletons::AccountManager::getInstance(); - QString userId; - QString userNickname; - auto currentTwitchUser = accountManager.Twitch.getCurrent(); - if (currentTwitchUser) { - userId = currentTwitchUser->getUserId(); - userNickname = currentTwitchUser->getNickName(); - } - QObject::connect(this->ui->profile, &QPushButton::clicked, this, [=]() { - QDesktopServices::openUrl(QUrl("https://twitch.tv/" + this->ui->lblUsername->text())); + QDesktopServices::openUrl(QUrl("https://twitch.tv/" + this->popupWidgetUser.username)); }); QObject::connect(this->ui->sendMessage, &QPushButton::clicked, this, [=]() { @@ -80,17 +91,41 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) [=]() { QApplication::clipboard()->setText(this->ui->lblUsername->text()); }); QObject::connect(this->ui->follow, &QPushButton::clicked, this, [=]() { - QUrl requestUrl("https://api.twitch.tv/kraken/users/" + userId + "/follows/channels/" + - this->userID); + debug::Log("Attempt to toggle follow user {}({}) as user {}({})", + this->popupWidgetUser.username, this->popupWidgetUser.userID, + this->loggedInUser.username, this->loggedInUser.userID); - util::twitch::put(requestUrl, - [](QJsonObject obj) { qDebug() << "follows channel: " << obj; }); + QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->loggedInUser.userID + + "/follows/channels/" + this->popupWidgetUser.userID); + + this->ui->follow->setEnabled(false); + if (!this->relationship.following) { + util::twitch::put(requestUrl, [this](QJsonObject obj) { + qDebug() << "follows channel: " << obj; + this->relationship.following = true; + emit refreshButtons(); + }); + } else { + util::twitch::sendDelete(requestUrl, [this] { + this->relationship.following = false; + emit refreshButtons(); + }); + } }); QObject::connect(this->ui->ignore, &QPushButton::clicked, this, [=]() { - QUrl requestUrl("https://api.twitch.tv/kraken/users/" + userId + "/blocks/" + this->userID); + QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->loggedInUser.userID + + "/blocks/" + this->popupWidgetUser.userID); - util::twitch::put(requestUrl, [](QJsonObject obj) { qDebug() << "blocks user: " << obj; }); + if (!this->relationship.ignoring) { + util::twitch::put(requestUrl, [this](auto) { + this->relationship.ignoring = true; // + }); + } else { + util::twitch::sendDelete(requestUrl, [this] { + this->relationship.ignoring = false; // + }); + } }); QObject::connect(this->ui->disableHighlights, &QPushButton::clicked, this, [=, &settings]() { @@ -119,16 +154,32 @@ AccountPopupWidget::AccountPopupWidget(SharedChannel _channel) this->hide(); // }); - util::twitch::getUserID(userNickname, this, - [=](const QString &id) { currentTwitchUser->setUserId(id); }); - this->dpiMultiplierChanged(this->getDpiMultiplier(), this->getDpiMultiplier()); } void AccountPopupWidget::setName(const QString &name) { + this->relationship.following = false; + this->relationship.ignoring = false; + + this->popupWidgetUser.username = name; this->ui->lblUsername->setText(name); this->getUserId(); + + // Refresh popup widget users type + + this->popupWidgetUser.refreshUserType(this->channel, false); +} + +void AccountPopupWidget::User::refreshUserType(const SharedChannel &channel, bool loggedInUser) +{ + if (channel->name == this->username) { + this->userType = UserType::Owner; + } else if ((loggedInUser && channel->isMod()) || channel->modList.contains(this->username)) { + this->userType = UserType::Mod; + } else { + this->userType = UserType::User; + } } void AccountPopupWidget::setChannel(SharedChannel _channel) @@ -138,8 +189,8 @@ void AccountPopupWidget::setChannel(SharedChannel _channel) void AccountPopupWidget::getUserId() { - util::twitch::getUserID(this->ui->lblUsername->text(), this, [=](const QString &id) { - userID = id; + util::twitch::getUserID(this->popupWidgetUser.username, this, [=](const QString &id) { + this->popupWidgetUser.userID = id; this->getUserData(); }); } @@ -147,18 +198,31 @@ void AccountPopupWidget::getUserId() void AccountPopupWidget::getUserData() { util::twitch::get( - "https://api.twitch.tv/kraken/channels/" + this->userID, this, [=](const QJsonObject &obj) { + "https://api.twitch.tv/kraken/channels/" + this->popupWidgetUser.userID, this, + [=](const QJsonObject &obj) { this->ui->lblFollowers->setText(QString::number(obj.value("followers").toInt())); this->ui->lblViews->setText(QString::number(obj.value("views").toInt())); this->ui->lblAccountAge->setText(obj.value("created_at").toString().section("T", 0, 0)); this->loadAvatar(QUrl(obj.value("logo").toString())); }); + + util::twitch::get("https://api.twitch.tv/kraken/users/" + this->loggedInUser.userID + + "/follows/channels/" + this->popupWidgetUser.userID, + this, [=](const QJsonObject &obj) { + this->ui->follow->setEnabled(true); + this->relationship.following = obj.contains("channel"); + + emit refreshButtons(); + }); + + // TODO: Get ignore relationship between logged in user and popup widget user and update + // relationship.ignoring } void AccountPopupWidget::loadAvatar(const QUrl &avatarUrl) { - if (!this->avatarMap.tryGet(this->userID, this->avatar)) { + if (!this->avatarMap.tryGet(this->popupWidgetUser.userID, this->avatar)) { if (!avatarUrl.isEmpty()) { QNetworkRequest req(avatarUrl); static auto manager = new QNetworkAccessManager(); @@ -168,7 +232,7 @@ void AccountPopupWidget::loadAvatar(const QUrl &avatarUrl) if (reply->error() == QNetworkReply::NoError) { const auto data = reply->readAll(); this->avatar.loadFromData(data); - this->avatarMap.insert(this->userID, this->avatar); + this->avatarMap.insert(this->popupWidgetUser.userID, this->avatar); this->ui->lblAvatar->setPixmap(this->avatar); } else { this->ui->lblAvatar->setText("ERROR"); @@ -182,24 +246,6 @@ void AccountPopupWidget::loadAvatar(const QUrl &avatarUrl) } } -void AccountPopupWidget::updatePermissions() -{ - singletons::AccountManager &accountManager = singletons::AccountManager::getInstance(); - - auto currentTwitchUser = accountManager.Twitch.getCurrent(); - if (!currentTwitchUser) { - // No twitch user set (should never happen) - return; - } - - if (this->channel.get()->name == currentTwitchUser->getNickName()) { - this->permission = permissions::Owner; - } else if (this->channel->modList.contains(currentTwitchUser->getNickName())) { - // XXX(pajlada): This might always trigger if user is anonymous (if nickName is empty?) - this->permission = permissions::Mod; - } -} - void AccountPopupWidget::dpiMultiplierChanged(float /*oldDpi*/, float newDpi) { this->setStyleSheet(QString("* { font-size: px; }") @@ -230,6 +276,84 @@ void AccountPopupWidget::sendCommand(QPushButton *button, QString command) }); } +void AccountPopupWidget::refreshLayouts() +{ + singletons::AccountManager &accountManager = singletons::AccountManager::getInstance(); + auto currentTwitchUser = accountManager.Twitch.getCurrent(); + if (!currentTwitchUser) { + // No twitch user set (should never happen) + return; + } + + QString loggedInUsername = currentTwitchUser->getUserName(); + QString popupUsername = this->ui->lblUsername->text(); + + bool showModLayout = false; + bool showUserLayout = false; + bool showOwnerLayout = false; + + if (loggedInUsername == popupUsername) { + // Clicked user is the same as the logged in user + showModLayout = false; + showUserLayout = false; + showOwnerLayout = false; + } else { + showUserLayout = true; + + switch (this->loggedInUser.userType) { + case UserType::Mod: { + showModLayout = true; + } break; + + case UserType::Owner: { + showModLayout = true; + showOwnerLayout = true; + } break; + } + } + + if (this->popupWidgetUser.userType == UserType::Owner) { + showModLayout = false; + showOwnerLayout = false; + } + + if (this->popupWidgetUser.userType == UserType::Mod && + this->loggedInUser.userType != UserType::Owner) { + showModLayout = false; + } + + this->updateButtons(this->ui->modLayout, showModLayout); + this->updateButtons(this->ui->userLayout, showUserLayout); + this->updateButtons(this->ui->ownerLayout, showOwnerLayout); +} + +void AccountPopupWidget::actuallyRefreshButtons() +{ + if (this->relationship.following) { + if (this->ui->follow->text() != "Unfollow") { + this->ui->follow->setText("Unfollow"); + this->ui->follow->setEnabled(true); + } + } else { + if (this->ui->follow->text() != "Follow") { + this->ui->follow->setText("Follow"); + this->ui->follow->setEnabled(true); + } + } + + if (this->relationship.ignoring) { + if (this->ui->ignore->text() != "Unignore") { + this->ui->ignore->setText("Unignore"); + this->ui->ignore->setEnabled(true); + } + } else { + if (this->ui->ignore->text() != "Ignore") { + this->ui->ignore->setText("Ignore"); + this->ui->ignore->setEnabled(true); + } + } +} + void AccountPopupWidget::focusOutEvent(QFocusEvent *) { this->hide(); @@ -242,29 +366,16 @@ void AccountPopupWidget::focusOutEvent(QFocusEvent *) void AccountPopupWidget::showEvent(QShowEvent *) { - singletons::AccountManager &accountManager = singletons::AccountManager::getInstance(); - auto currentTwitchUser = accountManager.Twitch.getCurrent(); - if (!currentTwitchUser) { - // No twitch user set (should never happen) - return; - } + this->loggedInUser.refreshUserType(this->channel, true); + this->popupWidgetUser.refreshUserType(this->channel, false); - if (this->ui->lblUsername->text() != currentTwitchUser->getNickName()) { - this->updateButtons(this->ui->userLayout, true); - if (this->permission != permissions::User) { - if (!this->channel->modList.contains(this->ui->lblUsername->text())) { - this->updateButtons(this->ui->modLayout, true); - } - if (this->permission == permissions::Owner) { - this->updateButtons(this->ui->ownerLayout, true); - this->updateButtons(this->ui->modLayout, true); - } - } - } else { - this->updateButtons(this->ui->modLayout, false); - this->updateButtons(this->ui->userLayout, false); - this->updateButtons(this->ui->ownerLayout, false); - } + this->ui->follow->setEnabled(false); + // XXX: Uncomment when ignore/unignore is fully implemented + // this->ui->ignore->setEnabled(false); + + this->refreshButtons(); + + this->refreshLayouts(); QString blacklisted = singletons::SettingManager::getInstance().highlightUserBlacklist; QStringList list = blacklisted.split("\n", QString::SkipEmptyParts); diff --git a/src/widgets/accountpopup.hpp b/src/widgets/accountpopup.hpp index 6d53bd3a0..e5bae7d1a 100644 --- a/src/widgets/accountpopup.hpp +++ b/src/widgets/accountpopup.hpp @@ -28,7 +28,11 @@ public: void setName(const QString &name); void setChannel(SharedChannel _channel); - void updatePermissions(); +public slots: + void actuallyRefreshButtons(); + +signals: + void refreshButtons(); protected: virtual void dpiMultiplierChanged(float oldDpi, float newDpi) override; @@ -44,16 +48,33 @@ private: void timeout(QPushButton *button, int time); void sendCommand(QPushButton *button, QString command); - enum class permissions { User, Mod, Owner }; - permissions permission; + void refreshLayouts(); + + enum class UserType { User, Mod, Owner }; SharedChannel channel; - QString userID; QPixmap avatar; util::ConcurrentMap avatarMap; + struct User { + QString username; + QString userID; + UserType userType = UserType::User; + + void refreshUserType(const SharedChannel &channel, bool loggedInUser); + }; + + User loggedInUser; + + User popupWidgetUser; + + struct { + bool following = false; + bool ignoring = false; + } relationship; + protected: virtual void focusOutEvent(QFocusEvent *event) override; virtual void showEvent(QShowEvent *event) override; diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 998d48a45..6f691502c 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -787,7 +787,6 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) auto user = link.getValue(); this->userPopupWidget.setName(user); this->userPopupWidget.move(event->screenPos().toPoint()); - this->userPopupWidget.updatePermissions(); this->userPopupWidget.show(); this->userPopupWidget.setFocus(); diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index 55d29fa94..30085eae3 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -539,7 +539,6 @@ void Split::doOpenAccountPopupWidget(AccountPopupWidget *widget, QString user) { widget->setName(user); widget->move(QCursor::pos()); - widget->updatePermissions(); widget->show(); widget->setFocus(); }