refactor: make chatter list its own widget (#6365)
This commit is contained in:
@@ -76,6 +76,7 @@
|
|||||||
- Dev: Don't detach threads. (#6333)
|
- Dev: Don't detach threads. (#6333)
|
||||||
- Dev: Mini refactor of `TwitchAccount`. (#6182)
|
- Dev: Mini refactor of `TwitchAccount`. (#6182)
|
||||||
- Dev: Refactored away some `getApp` usages in `WindowManager`. (#6194)
|
- Dev: Refactored away some `getApp` usages in `WindowManager`. (#6194)
|
||||||
|
- Dev: Refactored the Chatter List window into its own widget. (#6365)
|
||||||
- Dev: Simplified string literals to be a re-export of Qt functions. (#6175)
|
- Dev: Simplified string literals to be a re-export of Qt functions. (#6175)
|
||||||
- Dev: Fixed incorrect lua generation of static methods for typescript plugins. (#6190, #6223)
|
- Dev: Fixed incorrect lua generation of static methods for typescript plugins. (#6190, #6223)
|
||||||
- Dev: Merged top/bottom and left/right notebook layouts. (#6215)
|
- Dev: Merged top/bottom and left/right notebook layouts. (#6215)
|
||||||
|
|||||||
@@ -590,6 +590,8 @@ set(SOURCE_FILES
|
|||||||
widgets/BaseWidget.hpp
|
widgets/BaseWidget.hpp
|
||||||
widgets/BaseWindow.cpp
|
widgets/BaseWindow.cpp
|
||||||
widgets/BaseWindow.hpp
|
widgets/BaseWindow.hpp
|
||||||
|
widgets/ChatterListWidget.cpp
|
||||||
|
widgets/ChatterListWidget.hpp
|
||||||
widgets/DraggablePopup.cpp
|
widgets/DraggablePopup.cpp
|
||||||
widgets/DraggablePopup.hpp
|
widgets/DraggablePopup.hpp
|
||||||
widgets/FramelessEmbedWindow.cpp
|
widgets/FramelessEmbedWindow.cpp
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
#include "widgets/ChatterListWidget.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||||
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
|
#include "providers/twitch/TwitchAccount.hpp" // IWYU pragma: keep
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
#include "singletons/Fonts.hpp"
|
||||||
|
#include "singletons/Theme.hpp"
|
||||||
|
#include "util/Helpers.hpp"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString formatVIPListError(HelixListVIPsError error, const QString &message)
|
||||||
|
{
|
||||||
|
using Error = HelixListVIPsError;
|
||||||
|
|
||||||
|
QString errorMessage = QString("Failed to list VIPs - ");
|
||||||
|
|
||||||
|
switch (error)
|
||||||
|
{
|
||||||
|
case Error::Forwarded: {
|
||||||
|
errorMessage += message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::Ratelimited: {
|
||||||
|
errorMessage += "You are being ratelimited by Twitch. Try "
|
||||||
|
"again in a few seconds.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserMissingScope: {
|
||||||
|
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
|
||||||
|
errorMessage += "Missing required scope. "
|
||||||
|
"Re-login with your "
|
||||||
|
"account and try again.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserNotAuthorized: {
|
||||||
|
// TODO(pajlada): Phrase MISSING_PERMISSION
|
||||||
|
errorMessage += "You don't have permission to "
|
||||||
|
"perform that action.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserNotBroadcaster: {
|
||||||
|
errorMessage +=
|
||||||
|
"Due to Twitch restrictions, "
|
||||||
|
"this command can only be used by the broadcaster. "
|
||||||
|
"To see the list of VIPs you must use the Twitch website.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::Unknown: {
|
||||||
|
errorMessage += "An unknown error has occurred.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatModsError(HelixGetModeratorsError error, const QString &message)
|
||||||
|
{
|
||||||
|
using Error = HelixGetModeratorsError;
|
||||||
|
|
||||||
|
QString errorMessage = QString("Failed to get moderators: ");
|
||||||
|
|
||||||
|
switch (error)
|
||||||
|
{
|
||||||
|
case Error::Forwarded: {
|
||||||
|
errorMessage += message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserMissingScope: {
|
||||||
|
errorMessage += "Missing required scope. "
|
||||||
|
"Re-login with your "
|
||||||
|
"account and try again.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserNotAuthorized: {
|
||||||
|
errorMessage +=
|
||||||
|
"Due to Twitch restrictions, "
|
||||||
|
"this command can only be used by the broadcaster. "
|
||||||
|
"To see the list of mods you must use the Twitch website.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::Unknown: {
|
||||||
|
errorMessage += "An unknown error has occurred.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatChattersError(HelixGetChattersError error, const QString &message)
|
||||||
|
{
|
||||||
|
using Error = HelixGetChattersError;
|
||||||
|
|
||||||
|
QString errorMessage = QString("Failed to get chatters: ");
|
||||||
|
|
||||||
|
switch (error)
|
||||||
|
{
|
||||||
|
case Error::Forwarded: {
|
||||||
|
errorMessage += message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserMissingScope: {
|
||||||
|
errorMessage += "Missing required scope. "
|
||||||
|
"Re-login with your "
|
||||||
|
"account and try again.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::UserNotAuthorized: {
|
||||||
|
errorMessage +=
|
||||||
|
"Due to Twitch restrictions, "
|
||||||
|
"this command can only be used by moderators. "
|
||||||
|
"To see the list of chatters you must use the Twitch website.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Error::Unknown: {
|
||||||
|
errorMessage += "An unknown error has occurred.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ChatterListWidget::ChatterListWidget(const TwitchChannel *twitchChannel,
|
||||||
|
QWidget *parent)
|
||||||
|
: BaseWindow({}, parent)
|
||||||
|
{
|
||||||
|
this->setWindowTitle("Chatter List - " + twitchChannel->getName());
|
||||||
|
assert(twitchChannel != nullptr);
|
||||||
|
|
||||||
|
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
auto *dockVbox = new QVBoxLayout();
|
||||||
|
auto *searchBar = new QLineEdit(this);
|
||||||
|
|
||||||
|
auto *chattersList = new QListWidget();
|
||||||
|
auto *resultList = new QListWidget();
|
||||||
|
|
||||||
|
auto *loadingLabel = new QLabel("Loading...");
|
||||||
|
searchBar->setPlaceholderText("Search User...");
|
||||||
|
|
||||||
|
auto formatListItemText = [](const QString &text) {
|
||||||
|
auto *item = new QListWidgetItem();
|
||||||
|
item->setText(text);
|
||||||
|
item->setFont(
|
||||||
|
getApp()->getFonts()->getFont(FontStyle::ChatMedium, 1.0));
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addLabel = [this, formatListItemText,
|
||||||
|
chattersList](const QString &label) {
|
||||||
|
auto *formattedLabel = formatListItemText(label);
|
||||||
|
formattedLabel->setFlags(Qt::NoItemFlags);
|
||||||
|
formattedLabel->setForeground(this->theme->accent);
|
||||||
|
chattersList->addItem(formattedLabel);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addUserList = [=](const QStringList &users, QString label) {
|
||||||
|
if (users.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLabel(QString("%1 (%2)").arg(label, localizeNumbers(users.size())));
|
||||||
|
|
||||||
|
for (const auto &user : users)
|
||||||
|
{
|
||||||
|
chattersList->addItem(formatListItemText(user));
|
||||||
|
}
|
||||||
|
chattersList->addItem(new QListWidgetItem());
|
||||||
|
};
|
||||||
|
|
||||||
|
auto performListSearch = [=]() {
|
||||||
|
auto query = searchBar->text();
|
||||||
|
if (query.isEmpty())
|
||||||
|
{
|
||||||
|
resultList->hide();
|
||||||
|
chattersList->show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto results = chattersList->findItems(query, Qt::MatchContains);
|
||||||
|
chattersList->hide();
|
||||||
|
resultList->clear();
|
||||||
|
for (auto &item : results)
|
||||||
|
{
|
||||||
|
if (!item->text().contains("("))
|
||||||
|
{
|
||||||
|
resultList->addItem(formatListItemText(item->text()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultList->show();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto loadChatters = [=](auto modList, auto vipList, bool isBroadcaster) {
|
||||||
|
getHelix()->getChatters(
|
||||||
|
twitchChannel->roomId(),
|
||||||
|
getApp()->getAccounts()->twitch.getCurrent()->getUserId(), 50000,
|
||||||
|
[=](const auto &chatters) {
|
||||||
|
auto broadcaster = twitchChannel->getName().toLower();
|
||||||
|
QStringList chatterList;
|
||||||
|
QStringList modChatters;
|
||||||
|
QStringList vipChatters;
|
||||||
|
|
||||||
|
bool addedBroadcaster = false;
|
||||||
|
for (auto chatter : chatters.chatters)
|
||||||
|
{
|
||||||
|
chatter = chatter.toLower();
|
||||||
|
|
||||||
|
if (!addedBroadcaster && chatter == broadcaster)
|
||||||
|
{
|
||||||
|
addedBroadcaster = true;
|
||||||
|
addLabel("Broadcaster");
|
||||||
|
chattersList->addItem(broadcaster);
|
||||||
|
chattersList->addItem(new QListWidgetItem());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modList.contains(chatter))
|
||||||
|
{
|
||||||
|
modChatters.append(chatter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vipList.contains(chatter))
|
||||||
|
{
|
||||||
|
vipChatters.append(chatter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatterList.append(chatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
modChatters.sort();
|
||||||
|
vipChatters.sort();
|
||||||
|
chatterList.sort();
|
||||||
|
|
||||||
|
if (isBroadcaster)
|
||||||
|
{
|
||||||
|
addUserList(modChatters, QString("Moderators"));
|
||||||
|
addUserList(vipChatters, QString("VIPs"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addLabel("Moderators");
|
||||||
|
chattersList->addItem(
|
||||||
|
"Moderators cannot check who is a moderator");
|
||||||
|
chattersList->addItem(new QListWidgetItem());
|
||||||
|
|
||||||
|
addLabel("VIPs");
|
||||||
|
chattersList->addItem(
|
||||||
|
"Moderators cannot check who is a VIP");
|
||||||
|
chattersList->addItem(new QListWidgetItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserList(chatterList, QString("Chatters"));
|
||||||
|
|
||||||
|
loadingLabel->hide();
|
||||||
|
performListSearch();
|
||||||
|
},
|
||||||
|
[chattersList, formatListItemText](auto error,
|
||||||
|
const auto &message) {
|
||||||
|
auto errorMessage = formatChattersError(error, message);
|
||||||
|
chattersList->addItem(formatListItemText(errorMessage));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
QObject::connect(searchBar, &QLineEdit::textEdited, this,
|
||||||
|
performListSearch);
|
||||||
|
|
||||||
|
// Only broadcaster can get vips, mods can get chatters
|
||||||
|
if (twitchChannel->isBroadcaster())
|
||||||
|
{
|
||||||
|
// Add moderators
|
||||||
|
getHelix()->getModerators(
|
||||||
|
twitchChannel->roomId(), 1000,
|
||||||
|
[=](const auto &mods) {
|
||||||
|
QSet<QString> modList;
|
||||||
|
for (const auto &mod : mods)
|
||||||
|
{
|
||||||
|
modList.insert(mod.userName.toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add vips
|
||||||
|
getHelix()->getChannelVIPs(
|
||||||
|
twitchChannel->roomId(),
|
||||||
|
[=](const auto &vips) {
|
||||||
|
QSet<QString> vipList;
|
||||||
|
for (const auto &vip : vips)
|
||||||
|
{
|
||||||
|
vipList.insert(vip.userName.toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add chatters
|
||||||
|
loadChatters(modList, vipList, true);
|
||||||
|
},
|
||||||
|
[chattersList, formatListItemText](auto error,
|
||||||
|
const auto &message) {
|
||||||
|
auto errorMessage = formatVIPListError(error, message);
|
||||||
|
chattersList->addItem(formatListItemText(errorMessage));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[chattersList, formatListItemText](auto error,
|
||||||
|
const auto &message) {
|
||||||
|
auto errorMessage = formatModsError(error, message);
|
||||||
|
chattersList->addItem(formatListItemText(errorMessage));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (twitchChannel->hasModRights())
|
||||||
|
{
|
||||||
|
QSet<QString> modList;
|
||||||
|
QSet<QString> vipList;
|
||||||
|
loadChatters(modList, vipList, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chattersList->addItem(
|
||||||
|
formatListItemText("Due to Twitch restrictions, this feature is "
|
||||||
|
"only \navailable for moderators."));
|
||||||
|
chattersList->addItem(
|
||||||
|
formatListItemText("If you would like to see the Chatter list, you "
|
||||||
|
"must \nuse the Twitch website."));
|
||||||
|
loadingLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setMinimumWidth(300);
|
||||||
|
|
||||||
|
auto listDoubleClick = [this](const QModelIndex &index) {
|
||||||
|
const auto itemText = index.data().toString();
|
||||||
|
|
||||||
|
if (!itemText.isEmpty())
|
||||||
|
{
|
||||||
|
this->userClicked(itemText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QObject::connect(chattersList, &QListWidget::doubleClicked, this,
|
||||||
|
listDoubleClick);
|
||||||
|
|
||||||
|
QObject::connect(resultList, &QListWidget::doubleClicked, this,
|
||||||
|
listDoubleClick);
|
||||||
|
|
||||||
|
HotkeyController::HotkeyMap actions{
|
||||||
|
{"delete",
|
||||||
|
[this](const std::vector<QString> &) -> QString {
|
||||||
|
this->close();
|
||||||
|
return "";
|
||||||
|
}},
|
||||||
|
{"accept", nullptr},
|
||||||
|
{"reject", nullptr},
|
||||||
|
{"scrollPage", nullptr},
|
||||||
|
{"openTab", nullptr},
|
||||||
|
{"search",
|
||||||
|
[searchBar](const std::vector<QString> &) -> QString {
|
||||||
|
searchBar->setFocus();
|
||||||
|
searchBar->selectAll();
|
||||||
|
return "";
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
|
||||||
|
getApp()->getHotkeys()->shortcutsForCategory(HotkeyCategory::PopupWindow,
|
||||||
|
actions, this);
|
||||||
|
|
||||||
|
dockVbox->addWidget(searchBar);
|
||||||
|
dockVbox->addWidget(loadingLabel);
|
||||||
|
dockVbox->addWidget(chattersList);
|
||||||
|
dockVbox->addWidget(resultList);
|
||||||
|
resultList->hide();
|
||||||
|
|
||||||
|
this->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||||
|
this->setLayout(dockVbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "widgets/BaseWindow.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class TwitchChannel;
|
||||||
|
|
||||||
|
class ChatterListWidget : public BaseWindow
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ChatterListWidget(const TwitchChannel *twitchChannel, QWidget *parent);
|
||||||
|
|
||||||
|
Q_SIGNAL void userClicked(QString userLogin);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
+25
-392
@@ -8,7 +8,6 @@
|
|||||||
#include "controllers/commands/CommandController.hpp"
|
#include "controllers/commands/CommandController.hpp"
|
||||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||||
#include "controllers/notifications/NotificationController.hpp"
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
@@ -18,8 +17,8 @@
|
|||||||
#include "singletons/Theme.hpp"
|
#include "singletons/Theme.hpp"
|
||||||
#include "singletons/WindowManager.hpp"
|
#include "singletons/WindowManager.hpp"
|
||||||
#include "util/CustomPlayer.hpp"
|
#include "util/CustomPlayer.hpp"
|
||||||
#include "util/Helpers.hpp"
|
|
||||||
#include "util/StreamLink.hpp"
|
#include "util/StreamLink.hpp"
|
||||||
|
#include "widgets/ChatterListWidget.hpp"
|
||||||
#include "widgets/dialogs/SelectChannelDialog.hpp"
|
#include "widgets/dialogs/SelectChannelDialog.hpp"
|
||||||
#include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
|
#include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
|
||||||
#include "widgets/dialogs/UserInfoPopup.hpp"
|
#include "widgets/dialogs/UserInfoPopup.hpp"
|
||||||
@@ -40,7 +39,6 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDockWidget>
|
|
||||||
#include <QDrag>
|
#include <QDrag>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
@@ -53,134 +51,6 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using namespace chatterino;
|
|
||||||
|
|
||||||
QString formatVIPListError(HelixListVIPsError error, const QString &message)
|
|
||||||
{
|
|
||||||
using Error = HelixListVIPsError;
|
|
||||||
|
|
||||||
QString errorMessage = QString("Failed to list VIPs - ");
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case Error::Forwarded: {
|
|
||||||
errorMessage += message;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Ratelimited: {
|
|
||||||
errorMessage += "You are being ratelimited by Twitch. Try "
|
|
||||||
"again in a few seconds.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserMissingScope: {
|
|
||||||
// TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE
|
|
||||||
errorMessage += "Missing required scope. "
|
|
||||||
"Re-login with your "
|
|
||||||
"account and try again.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserNotAuthorized: {
|
|
||||||
// TODO(pajlada): Phrase MISSING_PERMISSION
|
|
||||||
errorMessage += "You don't have permission to "
|
|
||||||
"perform that action.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserNotBroadcaster: {
|
|
||||||
errorMessage +=
|
|
||||||
"Due to Twitch restrictions, "
|
|
||||||
"this command can only be used by the broadcaster. "
|
|
||||||
"To see the list of VIPs you must use the Twitch website.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Unknown: {
|
|
||||||
errorMessage += "An unknown error has occurred.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString formatModsError(HelixGetModeratorsError error, const QString &message)
|
|
||||||
{
|
|
||||||
using Error = HelixGetModeratorsError;
|
|
||||||
|
|
||||||
QString errorMessage = QString("Failed to get moderators: ");
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case Error::Forwarded: {
|
|
||||||
errorMessage += message;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserMissingScope: {
|
|
||||||
errorMessage += "Missing required scope. "
|
|
||||||
"Re-login with your "
|
|
||||||
"account and try again.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserNotAuthorized: {
|
|
||||||
errorMessage +=
|
|
||||||
"Due to Twitch restrictions, "
|
|
||||||
"this command can only be used by the broadcaster. "
|
|
||||||
"To see the list of mods you must use the Twitch website.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Unknown: {
|
|
||||||
errorMessage += "An unknown error has occurred.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString formatChattersError(HelixGetChattersError error, const QString &message)
|
|
||||||
{
|
|
||||||
using Error = HelixGetChattersError;
|
|
||||||
|
|
||||||
QString errorMessage = QString("Failed to get chatters: ");
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case Error::Forwarded: {
|
|
||||||
errorMessage += message;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserMissingScope: {
|
|
||||||
errorMessage += "Missing required scope. "
|
|
||||||
"Re-login with your "
|
|
||||||
"account and try again.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::UserNotAuthorized: {
|
|
||||||
errorMessage +=
|
|
||||||
"Due to Twitch restrictions, "
|
|
||||||
"this command can only be used by moderators. "
|
|
||||||
"To see the list of chatters you must use the Twitch website.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Error::Unknown: {
|
|
||||||
errorMessage += "An unknown error has occurred.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
namespace {
|
namespace {
|
||||||
void showTutorialVideo(QWidget *parent, const QString &source,
|
void showTutorialVideo(QWidget *parent, const QString &source,
|
||||||
@@ -681,7 +551,7 @@ void Split::addShortcuts()
|
|||||||
}},
|
}},
|
||||||
{"openViewerList",
|
{"openViewerList",
|
||||||
[this](const std::vector<QString> &) -> QString {
|
[this](const std::vector<QString> &) -> QString {
|
||||||
this->showChatterList();
|
this->openChatterList();
|
||||||
return "";
|
return "";
|
||||||
}},
|
}},
|
||||||
{"clearMessages",
|
{"clearMessages",
|
||||||
@@ -1147,14 +1017,20 @@ void Split::deleteFromContainer()
|
|||||||
|
|
||||||
void Split::changeChannel()
|
void Split::changeChannel()
|
||||||
{
|
{
|
||||||
this->showChangeChannelPopup("Change channel", false, [](bool) {});
|
this->showChangeChannelPopup(
|
||||||
|
"Change channel", false, [this](bool didSelectChannel) {
|
||||||
|
if (!didSelectChannel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto popup = this->findChildren<QDockWidget *>();
|
// After changing channel (i.e. pressing OK in the channel switcher), close all open Chatter Lists
|
||||||
if (popup.size() && popup.at(0)->isVisible() && !popup.at(0)->isFloating())
|
// We could consider updating the chatter list with the new channel
|
||||||
{
|
for (const auto &w : this->findChildren<ChatterListWidget *>())
|
||||||
popup.at(0)->hide();
|
{
|
||||||
showChatterList();
|
w->close();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Split::explainMoving()
|
void Split::explainMoving()
|
||||||
@@ -1253,26 +1129,8 @@ void Split::openWithCustomScheme()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Split::showChatterList()
|
void Split::openChatterList()
|
||||||
{
|
{
|
||||||
auto *chatterDock = new QDockWidget(
|
|
||||||
"Chatter List - " + this->getChannel()->getName(), this);
|
|
||||||
chatterDock->setAllowedAreas(Qt::LeftDockWidgetArea);
|
|
||||||
chatterDock->setFeatures(QDockWidget::DockWidgetVerticalTitleBar |
|
|
||||||
QDockWidget::DockWidgetClosable |
|
|
||||||
QDockWidget::DockWidgetFloatable);
|
|
||||||
chatterDock->resize(
|
|
||||||
0.5 * this->width(),
|
|
||||||
this->height() - this->header_->height() - this->input_->height());
|
|
||||||
chatterDock->move(0, this->header_->height());
|
|
||||||
|
|
||||||
auto *multiWidget = new QWidget(chatterDock);
|
|
||||||
auto *dockVbox = new QVBoxLayout();
|
|
||||||
auto *searchBar = new QLineEdit(chatterDock);
|
|
||||||
|
|
||||||
auto *chattersList = new QListWidget();
|
|
||||||
auto *resultList = new QListWidget();
|
|
||||||
|
|
||||||
auto channel = this->getChannel();
|
auto channel = this->getChannel();
|
||||||
if (!channel)
|
if (!channel)
|
||||||
{
|
{
|
||||||
@@ -1282,7 +1140,6 @@ void Split::showChatterList()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
||||||
|
|
||||||
if (twitchChannel == nullptr)
|
if (twitchChannel == nullptr)
|
||||||
{
|
{
|
||||||
qCWarning(chatterinoWidget)
|
qCWarning(chatterinoWidget)
|
||||||
@@ -1290,245 +1147,21 @@ void Split::showChatterList()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *loadingLabel = new QLabel("Loading...");
|
const auto chatterListWidth = static_cast<int>(this->width() * 0.5);
|
||||||
searchBar->setPlaceholderText("Search User...");
|
const auto chatterListHeight =
|
||||||
|
this->height() - this->header_->height() - this->input_->height();
|
||||||
|
|
||||||
auto formatListItemText = [](QString text) {
|
auto *chatterDock = new ChatterListWidget(twitchChannel, this);
|
||||||
auto *item = new QListWidgetItem();
|
|
||||||
item->setText(text);
|
|
||||||
item->setFont(
|
|
||||||
getApp()->getFonts()->getFont(FontStyle::ChatMedium, 1.0));
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto addLabel = [this, formatListItemText, chattersList](QString label) {
|
QObject::connect(chatterDock, &ChatterListWidget::userClicked,
|
||||||
auto *formattedLabel = formatListItemText(label);
|
[this](const QString &userLogin) {
|
||||||
formattedLabel->setForeground(this->theme->accent);
|
this->view_->showUserInfoPopup(userLogin);
|
||||||
chattersList->addItem(formattedLabel);
|
});
|
||||||
};
|
|
||||||
|
|
||||||
auto addUserList = [=](QStringList users, QString label) {
|
chatterDock->resize(chatterListWidth, chatterListHeight);
|
||||||
if (users.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLabel(QString("%1 (%2)").arg(label, localizeNumbers(users.size())));
|
|
||||||
|
|
||||||
for (const auto &user : users)
|
|
||||||
{
|
|
||||||
chattersList->addItem(formatListItemText(user));
|
|
||||||
}
|
|
||||||
chattersList->addItem(new QListWidgetItem());
|
|
||||||
};
|
|
||||||
|
|
||||||
auto performListSearch = [=]() {
|
|
||||||
auto query = searchBar->text();
|
|
||||||
if (query.isEmpty())
|
|
||||||
{
|
|
||||||
resultList->hide();
|
|
||||||
chattersList->show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto results = chattersList->findItems(query, Qt::MatchContains);
|
|
||||||
chattersList->hide();
|
|
||||||
resultList->clear();
|
|
||||||
for (auto &item : results)
|
|
||||||
{
|
|
||||||
if (!item->text().contains("("))
|
|
||||||
{
|
|
||||||
resultList->addItem(formatListItemText(item->text()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resultList->show();
|
|
||||||
};
|
|
||||||
|
|
||||||
auto loadChatters = [=](auto modList, auto vipList, bool isBroadcaster) {
|
|
||||||
getHelix()->getChatters(
|
|
||||||
twitchChannel->roomId(),
|
|
||||||
getApp()->getAccounts()->twitch.getCurrent()->getUserId(), 50000,
|
|
||||||
[=](auto chatters) {
|
|
||||||
auto broadcaster = channel->getName().toLower();
|
|
||||||
QStringList chatterList;
|
|
||||||
QStringList modChatters;
|
|
||||||
QStringList vipChatters;
|
|
||||||
|
|
||||||
bool addedBroadcaster = false;
|
|
||||||
for (auto chatter : chatters.chatters)
|
|
||||||
{
|
|
||||||
chatter = chatter.toLower();
|
|
||||||
|
|
||||||
if (!addedBroadcaster && chatter == broadcaster)
|
|
||||||
{
|
|
||||||
addedBroadcaster = true;
|
|
||||||
addLabel("Broadcaster");
|
|
||||||
chattersList->addItem(broadcaster);
|
|
||||||
chattersList->addItem(new QListWidgetItem());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modList.contains(chatter))
|
|
||||||
{
|
|
||||||
modChatters.append(chatter);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vipList.contains(chatter))
|
|
||||||
{
|
|
||||||
vipChatters.append(chatter);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatterList.append(chatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
modChatters.sort();
|
|
||||||
vipChatters.sort();
|
|
||||||
chatterList.sort();
|
|
||||||
|
|
||||||
if (isBroadcaster)
|
|
||||||
{
|
|
||||||
addUserList(modChatters, QString("Moderators"));
|
|
||||||
addUserList(vipChatters, QString("VIPs"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
addLabel("Moderators");
|
|
||||||
chattersList->addItem(
|
|
||||||
"Moderators cannot check who is a moderator");
|
|
||||||
chattersList->addItem(new QListWidgetItem());
|
|
||||||
|
|
||||||
addLabel("VIPs");
|
|
||||||
chattersList->addItem(
|
|
||||||
"Moderators cannot check who is a VIP");
|
|
||||||
chattersList->addItem(new QListWidgetItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
addUserList(chatterList, QString("Chatters"));
|
|
||||||
|
|
||||||
loadingLabel->hide();
|
|
||||||
performListSearch();
|
|
||||||
},
|
|
||||||
[chattersList, formatListItemText](auto error, auto message) {
|
|
||||||
auto errorMessage = formatChattersError(error, message);
|
|
||||||
chattersList->addItem(formatListItemText(errorMessage));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
QObject::connect(searchBar, &QLineEdit::textEdited, this,
|
|
||||||
performListSearch);
|
|
||||||
|
|
||||||
// Only broadcaster can get vips, mods can get chatters
|
|
||||||
if (channel->isBroadcaster())
|
|
||||||
{
|
|
||||||
// Add moderators
|
|
||||||
getHelix()->getModerators(
|
|
||||||
twitchChannel->roomId(), 1000,
|
|
||||||
[=](auto mods) {
|
|
||||||
QSet<QString> modList;
|
|
||||||
for (const auto &mod : mods)
|
|
||||||
{
|
|
||||||
modList.insert(mod.userName.toLower());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add vips
|
|
||||||
getHelix()->getChannelVIPs(
|
|
||||||
twitchChannel->roomId(),
|
|
||||||
[=](auto vips) {
|
|
||||||
QSet<QString> vipList;
|
|
||||||
for (const auto &vip : vips)
|
|
||||||
{
|
|
||||||
vipList.insert(vip.userName.toLower());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add chatters
|
|
||||||
loadChatters(modList, vipList, true);
|
|
||||||
},
|
|
||||||
[chattersList, formatListItemText](auto error,
|
|
||||||
auto message) {
|
|
||||||
auto errorMessage = formatVIPListError(error, message);
|
|
||||||
chattersList->addItem(formatListItemText(errorMessage));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[chattersList, formatListItemText](auto error, auto message) {
|
|
||||||
auto errorMessage = formatModsError(error, message);
|
|
||||||
chattersList->addItem(formatListItemText(errorMessage));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (channel->hasModRights())
|
|
||||||
{
|
|
||||||
QSet<QString> modList;
|
|
||||||
QSet<QString> vipList;
|
|
||||||
loadChatters(modList, vipList, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
chattersList->addItem(
|
|
||||||
formatListItemText("Due to Twitch restrictions, this feature is "
|
|
||||||
"only \navailable for moderators."));
|
|
||||||
chattersList->addItem(
|
|
||||||
formatListItemText("If you would like to see the Chatter list, you "
|
|
||||||
"must \nuse the Twitch website."));
|
|
||||||
loadingLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(chatterDock, &QDockWidget::topLevelChanged, this, [=]() {
|
|
||||||
chatterDock->setMinimumWidth(300);
|
|
||||||
});
|
|
||||||
|
|
||||||
auto listDoubleClick = [this](const QModelIndex &index) {
|
|
||||||
const auto itemText = index.data().toString();
|
|
||||||
|
|
||||||
// if the list item contains a parentheses it means that
|
|
||||||
// it's a category label so don't show a usercard
|
|
||||||
if (!itemText.contains("(") && !itemText.isEmpty())
|
|
||||||
{
|
|
||||||
this->view_->showUserInfoPopup(itemText);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QObject::connect(chattersList, &QListWidget::doubleClicked, this,
|
|
||||||
listDoubleClick);
|
|
||||||
|
|
||||||
QObject::connect(resultList, &QListWidget::doubleClicked, this,
|
|
||||||
listDoubleClick);
|
|
||||||
|
|
||||||
HotkeyController::HotkeyMap actions{
|
|
||||||
{"delete",
|
|
||||||
[chatterDock](const std::vector<QString> &) -> QString {
|
|
||||||
chatterDock->close();
|
|
||||||
return "";
|
|
||||||
}},
|
|
||||||
{"accept", nullptr},
|
|
||||||
{"reject", nullptr},
|
|
||||||
{"scrollPage", nullptr},
|
|
||||||
{"openTab", nullptr},
|
|
||||||
{"search",
|
|
||||||
[searchBar](const std::vector<QString> &) -> QString {
|
|
||||||
searchBar->setFocus();
|
|
||||||
searchBar->selectAll();
|
|
||||||
return "";
|
|
||||||
}},
|
|
||||||
};
|
|
||||||
|
|
||||||
getApp()->getHotkeys()->shortcutsForCategory(HotkeyCategory::PopupWindow,
|
|
||||||
actions, chatterDock);
|
|
||||||
|
|
||||||
dockVbox->addWidget(searchBar);
|
|
||||||
dockVbox->addWidget(loadingLabel);
|
|
||||||
dockVbox->addWidget(chattersList);
|
|
||||||
dockVbox->addWidget(resultList);
|
|
||||||
resultList->hide();
|
|
||||||
|
|
||||||
multiWidget->setStyleSheet(this->theme->splits.input.styleSheet);
|
|
||||||
multiWidget->setLayout(dockVbox);
|
|
||||||
chatterDock->setWidget(multiWidget);
|
|
||||||
chatterDock->setFloating(true);
|
|
||||||
widgets::showAndMoveWindowTo(
|
widgets::showAndMoveWindowTo(
|
||||||
chatterDock, this->mapToGlobal(QPoint{0, this->header_->height()}),
|
chatterDock, this->mapToGlobal(QPoint{0, this->header_->height()}),
|
||||||
widgets::BoundsChecking::CursorPosition);
|
widgets::BoundsChecking::CursorPosition);
|
||||||
chatterDock->activateWindow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Split::openSubPage()
|
void Split::openSubPage()
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ public Q_SLOTS:
|
|||||||
void openWithCustomScheme();
|
void openWithCustomScheme();
|
||||||
void setFiltersDialog();
|
void setFiltersDialog();
|
||||||
void showSearch(bool singleChannel);
|
void showSearch(bool singleChannel);
|
||||||
void showChatterList();
|
void openChatterList();
|
||||||
void openSubPage();
|
void openSubPage();
|
||||||
void reloadChannelAndSubscriberEmotes();
|
void reloadChannelAndSubscriberEmotes();
|
||||||
void reconnect();
|
void reconnect();
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ void SplitHeader::initializeLayout()
|
|||||||
// chatter list
|
// chatter list
|
||||||
this->chattersButton_ = makeWidget<PixmapButton>([&](auto w) {
|
this->chattersButton_ = makeWidget<PixmapButton>([&](auto w) {
|
||||||
QObject::connect(w, &Button::leftClicked, this, [this]() {
|
QObject::connect(w, &Button::leftClicked, this, [this]() {
|
||||||
this->split_->showChatterList();
|
this->split_->openChatterList();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
// dropdown
|
// dropdown
|
||||||
@@ -556,7 +556,7 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
|
|||||||
moreMenu->addAction(
|
moreMenu->addAction(
|
||||||
"Show chatter list",
|
"Show chatter list",
|
||||||
h->getDisplaySequence(HotkeyCategory::Split, "openViewerList"),
|
h->getDisplaySequence(HotkeyCategory::Split, "openViewerList"),
|
||||||
this->split_, &Split::showChatterList);
|
this->split_, &Split::openChatterList);
|
||||||
}
|
}
|
||||||
|
|
||||||
moreMenu->addAction("Subscribe",
|
moreMenu->addAction("Subscribe",
|
||||||
|
|||||||
Reference in New Issue
Block a user