mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
d2f1516818
* Specifically, this adds a caller to the network request, which makes the
success or failure callback not fire.
This has the unintended consequence of the block list not reloading if
the usercard is closed, but it's not a big concern.
* Add unrelated `-DUSE_ALTERNATE_LINKER` cmake option
From 0517d99b46/CMakeLists.txt (L87-L103)
1083 lines
39 KiB
C++
1083 lines
39 KiB
C++
#include "UserInfoPopup.hpp"
|
|
|
|
#include "Application.hpp"
|
|
#include "common/Channel.hpp"
|
|
#include "common/NetworkRequest.hpp"
|
|
#include "common/QLogging.hpp"
|
|
#include "controllers/accounts/AccountController.hpp"
|
|
#include "controllers/commands/CommandController.hpp"
|
|
#include "controllers/highlights/HighlightBlacklistUser.hpp"
|
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
|
#include "messages/Message.hpp"
|
|
#include "messages/MessageBuilder.hpp"
|
|
#include "providers/IvrApi.hpp"
|
|
#include "providers/twitch/api/Helix.hpp"
|
|
#include "providers/twitch/ChannelPointReward.hpp"
|
|
#include "providers/twitch/TwitchAccount.hpp"
|
|
#include "providers/twitch/TwitchChannel.hpp"
|
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
|
#include "singletons/Resources.hpp"
|
|
#include "singletons/Settings.hpp"
|
|
#include "singletons/Theme.hpp"
|
|
#include "singletons/WindowManager.hpp"
|
|
#include "util/Clipboard.hpp"
|
|
#include "util/Helpers.hpp"
|
|
#include "util/LayoutCreator.hpp"
|
|
#include "util/StreamerMode.hpp"
|
|
#include "widgets/helper/ChannelView.hpp"
|
|
#include "widgets/helper/EffectLabel.hpp"
|
|
#include "widgets/helper/Line.hpp"
|
|
#include "widgets/Label.hpp"
|
|
#include "widgets/Scrollbar.hpp"
|
|
#include "widgets/splits/Split.hpp"
|
|
#include "widgets/Window.hpp"
|
|
|
|
#include <QCheckBox>
|
|
#include <QDesktopServices>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QPointer>
|
|
|
|
const QString TEXT_FOLLOWERS("Followers: %1");
|
|
const QString TEXT_CREATED("Created: %1");
|
|
const QString TEXT_TITLE("%1's Usercard - #%2");
|
|
#define TEXT_USER_ID "ID: "
|
|
#define TEXT_UNAVAILABLE "(not available)"
|
|
|
|
namespace chatterino {
|
|
namespace {
|
|
Label *addCopyableLabel(LayoutCreator<QHBoxLayout> box, const char *tooltip,
|
|
Button **copyButton = nullptr)
|
|
{
|
|
auto label = box.emplace<Label>();
|
|
auto button = box.emplace<Button>();
|
|
if (copyButton != nullptr)
|
|
{
|
|
button.assign(copyButton);
|
|
}
|
|
button->setPixmap(getApp()->themes->buttons.copy);
|
|
button->setScaleIndependantSize(18, 18);
|
|
button->setDim(Button::Dim::Lots);
|
|
button->setToolTip(tooltip);
|
|
QObject::connect(
|
|
button.getElement(), &Button::leftClicked,
|
|
[label = label.getElement()] {
|
|
auto copyText = label->property("copy-text").toString();
|
|
|
|
crossPlatformCopy(copyText.isEmpty() ? label->getText()
|
|
: copyText);
|
|
});
|
|
|
|
return label.getElement();
|
|
};
|
|
|
|
bool checkMessageUserName(const QString &userName, MessagePtr message)
|
|
{
|
|
if (message->flags.has(MessageFlag::Whisper))
|
|
return false;
|
|
|
|
bool isSubscription = message->flags.has(MessageFlag::Subscription) &&
|
|
message->loginName.isEmpty() &&
|
|
message->messageText.split(" ").at(0).compare(
|
|
userName, Qt::CaseInsensitive) == 0;
|
|
|
|
bool isModAction =
|
|
message->timeoutUser.compare(userName, Qt::CaseInsensitive) == 0;
|
|
bool isSelectedUser =
|
|
message->loginName.compare(userName, Qt::CaseInsensitive) == 0;
|
|
|
|
return (isSubscription || isModAction || isSelectedUser);
|
|
}
|
|
|
|
ChannelPtr filterMessages(const QString &userName, ChannelPtr channel)
|
|
{
|
|
LimitedQueueSnapshot<MessagePtr> snapshot =
|
|
channel->getMessageSnapshot();
|
|
|
|
ChannelPtr channelPtr;
|
|
if (channel->isTwitchChannel())
|
|
{
|
|
channelPtr = std::make_shared<TwitchChannel>(channel->getName());
|
|
}
|
|
else
|
|
{
|
|
channelPtr = std::make_shared<Channel>(channel->getName(),
|
|
Channel::Type::None);
|
|
}
|
|
|
|
for (size_t i = 0; i < snapshot.size(); i++)
|
|
{
|
|
MessagePtr message = snapshot[i];
|
|
|
|
auto overrideFlags = boost::optional<MessageFlags>(message->flags);
|
|
overrideFlags->set(MessageFlag::DoNotLog);
|
|
|
|
if (checkMessageUserName(userName, message))
|
|
{
|
|
channelPtr->addMessage(message, overrideFlags);
|
|
}
|
|
}
|
|
|
|
return channelPtr;
|
|
};
|
|
|
|
const auto borderColor = QColor(255, 255, 255, 80);
|
|
|
|
int calculateTimeoutDuration(TimeoutButton timeout)
|
|
{
|
|
static const QMap<QString, int> durations{
|
|
{"s", 1}, {"m", 60}, {"h", 3600}, {"d", 86400}, {"w", 604800},
|
|
};
|
|
return timeout.second * durations[timeout.first];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
|
|
Split *split)
|
|
: DraggablePopup(closeAutomatically, parent)
|
|
, split_(split)
|
|
, closeAutomatically_(closeAutomatically)
|
|
{
|
|
assert(split != nullptr &&
|
|
"split being nullptr causes lots of bugs down the road");
|
|
this->setWindowTitle("Usercard");
|
|
this->setStayInScreenRect(true);
|
|
|
|
HotkeyController::HotkeyMap actions{
|
|
{"delete",
|
|
[this](std::vector<QString>) -> QString {
|
|
this->deleteLater();
|
|
return "";
|
|
}},
|
|
{"scrollPage",
|
|
[this](std::vector<QString> arguments) -> QString {
|
|
if (arguments.size() == 0)
|
|
{
|
|
qCWarning(chatterinoHotkeys)
|
|
<< "scrollPage hotkey called without arguments!";
|
|
return "scrollPage hotkey called without arguments!";
|
|
}
|
|
auto direction = arguments.at(0);
|
|
|
|
auto &scrollbar = this->ui_.latestMessages->getScrollBar();
|
|
if (direction == "up")
|
|
{
|
|
scrollbar.offset(-scrollbar.getLargeChange());
|
|
}
|
|
else if (direction == "down")
|
|
{
|
|
scrollbar.offset(scrollbar.getLargeChange());
|
|
}
|
|
else
|
|
{
|
|
qCWarning(chatterinoHotkeys) << "Unknown scroll direction";
|
|
}
|
|
return "";
|
|
}},
|
|
{"execModeratorAction",
|
|
[this](std::vector<QString> arguments) -> QString {
|
|
if (arguments.empty())
|
|
{
|
|
return "execModeratorAction action needs an argument, which "
|
|
"moderation action to execute, see description in the "
|
|
"editor";
|
|
}
|
|
auto target = arguments.at(0);
|
|
QString msg;
|
|
|
|
// these can't have /timeout/ buttons because they are not timeouts
|
|
if (target == "ban")
|
|
{
|
|
msg = QString("/ban %1").arg(this->userName_);
|
|
}
|
|
else if (target == "unban")
|
|
{
|
|
msg = QString("/unban %1").arg(this->userName_);
|
|
}
|
|
else
|
|
{
|
|
// find and execute timeout button #TARGET
|
|
|
|
bool ok;
|
|
int buttonNum = target.toInt(&ok);
|
|
if (!ok)
|
|
{
|
|
return QString("Invalid argument for execModeratorAction: "
|
|
"%1. Use "
|
|
"\"ban\", \"unban\" or the number of the "
|
|
"timeout "
|
|
"button to execute")
|
|
.arg(target);
|
|
}
|
|
|
|
const auto &timeoutButtons =
|
|
getSettings()->timeoutButtons.getValue();
|
|
if (timeoutButtons.size() < buttonNum || 0 >= buttonNum)
|
|
{
|
|
return QString("Invalid argument for execModeratorAction: "
|
|
"%1. Integer out of usable range: [1, %2]")
|
|
.arg(buttonNum, timeoutButtons.size() - 1);
|
|
}
|
|
const auto &button = timeoutButtons.at(buttonNum - 1);
|
|
msg = QString("/timeout %1 %2")
|
|
.arg(this->userName_)
|
|
.arg(calculateTimeoutDuration(button));
|
|
}
|
|
|
|
msg = getApp()->commands->execCommand(
|
|
msg, this->underlyingChannel_, false);
|
|
|
|
this->underlyingChannel_->sendMessage(msg);
|
|
return "";
|
|
}},
|
|
|
|
// these actions make no sense in the context of a usercard, so they aren't implemented
|
|
{"reject", nullptr},
|
|
{"accept", nullptr},
|
|
{"openTab", nullptr},
|
|
{"search", nullptr},
|
|
};
|
|
|
|
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
|
|
HotkeyCategory::PopupWindow, actions, this);
|
|
|
|
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
|
|
.setLayoutType<QVBoxLayout>();
|
|
|
|
// first line
|
|
auto head = layout.emplace<QHBoxLayout>().withoutMargin();
|
|
{
|
|
// avatar
|
|
auto avatar =
|
|
head.emplace<Button>(nullptr).assign(&this->ui_.avatarButton);
|
|
avatar->setScaleIndependantSize(100, 100);
|
|
avatar->setDim(Button::Dim::None);
|
|
QObject::connect(
|
|
avatar.getElement(), &Button::clicked,
|
|
[this](Qt::MouseButton button) {
|
|
switch (button)
|
|
{
|
|
case Qt::LeftButton: {
|
|
QDesktopServices::openUrl(QUrl(
|
|
"https://twitch.tv/" + this->userName_.toLower()));
|
|
}
|
|
break;
|
|
|
|
case Qt::RightButton: {
|
|
// don't raise open context menu if there's no avatar (probably in cases when invalid user's usercard was opened)
|
|
if (this->avatarUrl_.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
static QMenu *previousMenu = nullptr;
|
|
if (previousMenu != nullptr)
|
|
{
|
|
previousMenu->deleteLater();
|
|
previousMenu = nullptr;
|
|
}
|
|
|
|
auto menu = new QMenu;
|
|
previousMenu = menu;
|
|
|
|
auto avatarUrl = this->avatarUrl_;
|
|
|
|
// add context menu actions
|
|
menu->addAction("Open avatar in browser", [avatarUrl] {
|
|
QDesktopServices::openUrl(QUrl(avatarUrl));
|
|
});
|
|
|
|
menu->addAction("Copy avatar link", [avatarUrl] {
|
|
crossPlatformCopy(avatarUrl);
|
|
});
|
|
|
|
// we need to assign login name for msvc compilation
|
|
auto loginName = this->userName_.toLower();
|
|
menu->addAction(
|
|
"Open channel in a new popup window", this,
|
|
[loginName] {
|
|
auto app = getApp();
|
|
auto &window = app->windows->createWindow(
|
|
WindowType::Popup, true);
|
|
auto split = window.getNotebook()
|
|
.getOrAddSelectedPage()
|
|
->appendNewSplit(false);
|
|
split->setChannel(app->twitch->getOrAddChannel(
|
|
loginName.toLower()));
|
|
});
|
|
|
|
menu->addAction(
|
|
"Open channel in a new tab", this, [loginName] {
|
|
ChannelPtr channel =
|
|
getApp()->twitch->getOrAddChannel(
|
|
loginName);
|
|
auto &nb = getApp()
|
|
->windows->getMainWindow()
|
|
.getNotebook();
|
|
SplitContainer *container = nb.addPage(true);
|
|
Split *split = new Split(container);
|
|
split->setChannel(channel);
|
|
container->insertSplit(split);
|
|
});
|
|
menu->popup(QCursor::pos());
|
|
menu->raise();
|
|
}
|
|
break;
|
|
|
|
default:;
|
|
}
|
|
});
|
|
|
|
auto vbox = head.emplace<QVBoxLayout>();
|
|
{
|
|
// items on the right
|
|
{
|
|
auto box = vbox.emplace<QHBoxLayout>()
|
|
.withoutMargin()
|
|
.withoutSpacing();
|
|
|
|
this->ui_.nameLabel = addCopyableLabel(box, "Copy name");
|
|
this->ui_.nameLabel->setFontStyle(FontStyle::UiMediumBold);
|
|
box->addSpacing(5);
|
|
box->addStretch(1);
|
|
|
|
this->ui_.localizedNameLabel =
|
|
addCopyableLabel(box, "Copy localized name",
|
|
&this->ui_.localizedNameCopyButton);
|
|
this->ui_.localizedNameLabel->setFontStyle(
|
|
FontStyle::UiMediumBold);
|
|
box->addSpacing(5);
|
|
box->addStretch(1);
|
|
|
|
auto palette = QPalette();
|
|
palette.setColor(QPalette::WindowText, QColor("#aaa"));
|
|
this->ui_.userIDLabel = addCopyableLabel(box, "Copy ID");
|
|
this->ui_.userIDLabel->setPalette(palette);
|
|
|
|
this->ui_.localizedNameLabel->setVisible(false);
|
|
this->ui_.localizedNameCopyButton->setVisible(false);
|
|
|
|
// button to pin the window (only if we close automatically)
|
|
if (this->closeAutomatically_)
|
|
{
|
|
box->addWidget(this->createPinButton());
|
|
}
|
|
}
|
|
|
|
// items on the left
|
|
vbox.emplace<Label>(TEXT_FOLLOWERS.arg(""))
|
|
.assign(&this->ui_.followerCountLabel);
|
|
vbox.emplace<Label>(TEXT_CREATED.arg(""))
|
|
.assign(&this->ui_.createdDateLabel);
|
|
vbox.emplace<Label>("").assign(&this->ui_.followageLabel);
|
|
vbox.emplace<Label>("").assign(&this->ui_.subageLabel);
|
|
}
|
|
}
|
|
|
|
layout.emplace<Line>(false);
|
|
|
|
// second line
|
|
auto user = layout.emplace<QHBoxLayout>().withoutMargin();
|
|
{
|
|
user->addStretch(1);
|
|
|
|
user.emplace<QCheckBox>("Block").assign(&this->ui_.block);
|
|
user.emplace<QCheckBox>("Ignore highlights")
|
|
.assign(&this->ui_.ignoreHighlights);
|
|
auto usercard = user.emplace<EffectLabel2>(this);
|
|
usercard->getLabel().setText("Usercard");
|
|
auto mod = user.emplace<Button>(this);
|
|
mod->setPixmap(getResources().buttons.mod);
|
|
mod->setScaleIndependantSize(30, 30);
|
|
auto unmod = user.emplace<Button>(this);
|
|
unmod->setPixmap(getResources().buttons.unmod);
|
|
unmod->setScaleIndependantSize(30, 30);
|
|
auto vip = user.emplace<Button>(this);
|
|
vip->setPixmap(getResources().buttons.vip);
|
|
vip->setScaleIndependantSize(30, 30);
|
|
auto unvip = user.emplace<Button>(this);
|
|
unvip->setPixmap(getResources().buttons.unvip);
|
|
unvip->setScaleIndependantSize(30, 30);
|
|
|
|
user->addStretch(1);
|
|
|
|
QObject::connect(usercard.getElement(), &Button::leftClicked, [this] {
|
|
QDesktopServices::openUrl("https://www.twitch.tv/popout/" +
|
|
this->underlyingChannel_->getName() +
|
|
"/viewercard/" + this->userName_);
|
|
});
|
|
|
|
QObject::connect(mod.getElement(), &Button::leftClicked, [this] {
|
|
QString value = "/mod " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
this->underlyingChannel_->sendMessage(value);
|
|
});
|
|
QObject::connect(unmod.getElement(), &Button::leftClicked, [this] {
|
|
QString value = "/unmod " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
this->underlyingChannel_->sendMessage(value);
|
|
});
|
|
QObject::connect(vip.getElement(), &Button::leftClicked, [this] {
|
|
QString value = "/vip " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
this->underlyingChannel_->sendMessage(value);
|
|
});
|
|
QObject::connect(unvip.getElement(), &Button::leftClicked, [this] {
|
|
QString value = "/unvip " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
this->underlyingChannel_->sendMessage(value);
|
|
});
|
|
|
|
// userstate
|
|
this->userStateChanged_.connect([this, mod, unmod, vip,
|
|
unvip]() mutable {
|
|
TwitchChannel *twitchChannel =
|
|
dynamic_cast<TwitchChannel *>(this->underlyingChannel_.get());
|
|
|
|
bool visibilityModButtons = false;
|
|
|
|
if (twitchChannel)
|
|
{
|
|
bool isMyself =
|
|
QString::compare(
|
|
getApp()->accounts->twitch.getCurrent()->getUserName(),
|
|
this->userName_, Qt::CaseInsensitive) == 0;
|
|
|
|
visibilityModButtons =
|
|
twitchChannel->isBroadcaster() && !isMyself;
|
|
}
|
|
mod->setVisible(visibilityModButtons);
|
|
unmod->setVisible(visibilityModButtons);
|
|
vip->setVisible(visibilityModButtons);
|
|
unvip->setVisible(visibilityModButtons);
|
|
});
|
|
}
|
|
|
|
auto lineMod = layout.emplace<Line>(false);
|
|
|
|
// third line
|
|
auto moderation = layout.emplace<QHBoxLayout>().withoutMargin();
|
|
{
|
|
auto timeout = moderation.emplace<TimeoutWidget>();
|
|
|
|
this->userStateChanged_.connect([this, lineMod, timeout]() mutable {
|
|
TwitchChannel *twitchChannel =
|
|
dynamic_cast<TwitchChannel *>(this->underlyingChannel_.get());
|
|
|
|
bool hasModRights =
|
|
twitchChannel ? twitchChannel->hasModRights() : false;
|
|
lineMod->setVisible(hasModRights);
|
|
timeout->setVisible(hasModRights);
|
|
});
|
|
|
|
timeout->buttonClicked.connect([this](auto item) {
|
|
TimeoutWidget::Action action;
|
|
int arg;
|
|
std::tie(action, arg) = item;
|
|
|
|
switch (action)
|
|
{
|
|
case TimeoutWidget::Ban: {
|
|
if (this->underlyingChannel_)
|
|
{
|
|
QString value = "/ban " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
|
|
this->underlyingChannel_->sendMessage(value);
|
|
}
|
|
}
|
|
break;
|
|
case TimeoutWidget::Unban: {
|
|
if (this->underlyingChannel_)
|
|
{
|
|
QString value = "/unban " + this->userName_;
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
|
|
this->underlyingChannel_->sendMessage(value);
|
|
}
|
|
}
|
|
break;
|
|
case TimeoutWidget::Timeout: {
|
|
if (this->underlyingChannel_)
|
|
{
|
|
QString value = "/timeout " + this->userName_ + " " +
|
|
QString::number(arg);
|
|
|
|
value = getApp()->commands->execCommand(
|
|
value, this->underlyingChannel_, false);
|
|
|
|
this->underlyingChannel_->sendMessage(value);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
layout.emplace<Line>(false);
|
|
|
|
// fourth line (last messages)
|
|
auto logs = layout.emplace<QVBoxLayout>().withoutMargin();
|
|
{
|
|
this->ui_.noMessagesLabel = new Label("No recent messages");
|
|
this->ui_.noMessagesLabel->setVisible(false);
|
|
|
|
this->ui_.latestMessages =
|
|
new ChannelView(this, this->split_, ChannelView::Context::UserCard,
|
|
getSettings()->scrollbackUsercardLimit);
|
|
this->ui_.latestMessages->setMinimumSize(400, 275);
|
|
this->ui_.latestMessages->setSizePolicy(QSizePolicy::Expanding,
|
|
QSizePolicy::Expanding);
|
|
|
|
logs->addWidget(this->ui_.noMessagesLabel);
|
|
logs->addWidget(this->ui_.latestMessages);
|
|
logs->setAlignment(this->ui_.noMessagesLabel, Qt::AlignHCenter);
|
|
}
|
|
|
|
this->installEvents();
|
|
this->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Policy::Ignored);
|
|
}
|
|
|
|
void UserInfoPopup::themeChangedEvent()
|
|
{
|
|
BaseWindow::themeChangedEvent();
|
|
|
|
for (auto &&child : this->findChildren<QCheckBox *>())
|
|
{
|
|
child->setFont(getFonts()->getFont(FontStyle::UiMedium, this->scale()));
|
|
}
|
|
}
|
|
|
|
void UserInfoPopup::scaleChangedEvent(float /*scale*/)
|
|
{
|
|
themeChangedEvent();
|
|
|
|
QTimer::singleShot(20, this, [this] {
|
|
auto geo = this->geometry();
|
|
geo.setWidth(10);
|
|
geo.setHeight(10);
|
|
|
|
this->setGeometry(geo);
|
|
});
|
|
}
|
|
|
|
void UserInfoPopup::installEvents()
|
|
{
|
|
std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false);
|
|
|
|
// block
|
|
QObject::connect(
|
|
this->ui_.block, &QCheckBox::stateChanged,
|
|
[this](int newState) mutable {
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
const auto reenableBlockCheckbox = [this] {
|
|
this->ui_.block->setEnabled(true);
|
|
};
|
|
|
|
if (!this->ui_.block->isEnabled())
|
|
{
|
|
reenableBlockCheckbox();
|
|
return;
|
|
}
|
|
|
|
switch (newState)
|
|
{
|
|
case Qt::CheckState::Unchecked: {
|
|
this->ui_.block->setEnabled(false);
|
|
|
|
getApp()->accounts->twitch.getCurrent()->unblockUser(
|
|
this->userId_, this,
|
|
[this, reenableBlockCheckbox, currentUser] {
|
|
this->channel_->addMessage(makeSystemMessage(
|
|
QString("You successfully unblocked user %1")
|
|
.arg(this->userName_)));
|
|
reenableBlockCheckbox();
|
|
},
|
|
[this, reenableBlockCheckbox] {
|
|
this->channel_->addMessage(makeSystemMessage(
|
|
QString(
|
|
"User %1 couldn't be unblocked, an unknown "
|
|
"error occurred!")
|
|
.arg(this->userName_)));
|
|
reenableBlockCheckbox();
|
|
});
|
|
}
|
|
break;
|
|
|
|
case Qt::CheckState::PartiallyChecked: {
|
|
// We deliberately ignore this state
|
|
}
|
|
break;
|
|
|
|
case Qt::CheckState::Checked: {
|
|
this->ui_.block->setEnabled(false);
|
|
|
|
getApp()->accounts->twitch.getCurrent()->blockUser(
|
|
this->userId_, this,
|
|
[this, reenableBlockCheckbox, currentUser] {
|
|
this->channel_->addMessage(makeSystemMessage(
|
|
QString("You successfully blocked user %1")
|
|
.arg(this->userName_)));
|
|
reenableBlockCheckbox();
|
|
},
|
|
[this, reenableBlockCheckbox] {
|
|
this->channel_->addMessage(makeSystemMessage(
|
|
QString(
|
|
"User %1 couldn't be blocked, an unknown "
|
|
"error occurred!")
|
|
.arg(this->userName_)));
|
|
reenableBlockCheckbox();
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
// ignore highlights
|
|
QObject::connect(
|
|
this->ui_.ignoreHighlights, &QCheckBox::clicked,
|
|
[this](bool checked) mutable {
|
|
this->ui_.ignoreHighlights->setEnabled(false);
|
|
|
|
if (checked)
|
|
{
|
|
getSettings()->blacklistedUsers.insert(
|
|
HighlightBlacklistUser{this->userName_, false});
|
|
this->ui_.ignoreHighlights->setEnabled(true);
|
|
}
|
|
else
|
|
{
|
|
const auto &vector = getSettings()->blacklistedUsers.raw();
|
|
|
|
for (int i = 0; i < vector.size(); i++)
|
|
{
|
|
if (this->userName_ == vector[i].getPattern())
|
|
{
|
|
getSettings()->blacklistedUsers.removeAt(i);
|
|
i--;
|
|
}
|
|
}
|
|
if (getSettings()->isBlacklistedUser(this->userName_))
|
|
{
|
|
this->ui_.ignoreHighlights->setToolTip(
|
|
"Name matched by regex");
|
|
}
|
|
else
|
|
{
|
|
this->ui_.ignoreHighlights->setEnabled(true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void UserInfoPopup::setData(const QString &name, const ChannelPtr &channel)
|
|
{
|
|
this->setData(name, channel, channel);
|
|
}
|
|
|
|
void UserInfoPopup::setData(const QString &name,
|
|
const ChannelPtr &contextChannel,
|
|
const ChannelPtr &openingChannel)
|
|
{
|
|
this->userName_ = name;
|
|
this->channel_ = openingChannel;
|
|
|
|
if (!contextChannel->isEmpty())
|
|
{
|
|
this->underlyingChannel_ = contextChannel;
|
|
}
|
|
else
|
|
{
|
|
this->underlyingChannel_ = openingChannel;
|
|
}
|
|
|
|
this->setWindowTitle(
|
|
TEXT_TITLE.arg(name, this->underlyingChannel_->getName()));
|
|
|
|
this->ui_.nameLabel->setText(name);
|
|
this->ui_.nameLabel->setProperty("copy-text", name);
|
|
|
|
this->updateUserData();
|
|
|
|
this->userStateChanged_.invoke();
|
|
|
|
this->updateLatestMessages();
|
|
QTimer::singleShot(1, this, [this] {
|
|
this->setStayInScreenRect(true);
|
|
});
|
|
}
|
|
|
|
void UserInfoPopup::updateLatestMessages()
|
|
{
|
|
auto filteredChannel =
|
|
filterMessages(this->userName_, this->underlyingChannel_);
|
|
this->ui_.latestMessages->setChannel(filteredChannel);
|
|
this->ui_.latestMessages->setSourceChannel(this->underlyingChannel_);
|
|
|
|
const bool hasMessages = filteredChannel->hasMessages();
|
|
this->ui_.latestMessages->setVisible(hasMessages);
|
|
this->ui_.noMessagesLabel->setVisible(!hasMessages);
|
|
|
|
// shrink dialog in case ChannelView goes from visible to hidden
|
|
this->adjustSize();
|
|
|
|
this->refreshConnection_ =
|
|
std::make_unique<pajlada::Signals::ScopedConnection>(
|
|
this->underlyingChannel_->messageAppended.connect(
|
|
[this, hasMessages](auto message, auto) {
|
|
if (!checkMessageUserName(this->userName_, message))
|
|
return;
|
|
|
|
if (hasMessages)
|
|
{
|
|
// display message in ChannelView
|
|
this->ui_.latestMessages->channel()->addMessage(
|
|
message);
|
|
}
|
|
else
|
|
{
|
|
// The ChannelView is currently hidden, so manually refresh
|
|
// and display the latest messages
|
|
this->updateLatestMessages();
|
|
}
|
|
}));
|
|
}
|
|
|
|
void UserInfoPopup::updateUserData()
|
|
{
|
|
std::weak_ptr<bool> hack = this->lifetimeHack_;
|
|
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
|
|
|
const auto onUserFetchFailed = [this, hack] {
|
|
if (!hack.lock())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// this can occur when the account doesn't exist.
|
|
this->ui_.followerCountLabel->setText(
|
|
TEXT_FOLLOWERS.arg(TEXT_UNAVAILABLE));
|
|
this->ui_.createdDateLabel->setText(TEXT_CREATED.arg(TEXT_UNAVAILABLE));
|
|
|
|
this->ui_.nameLabel->setText(this->userName_);
|
|
|
|
this->ui_.userIDLabel->setText(QString("ID ") +
|
|
QString(TEXT_UNAVAILABLE));
|
|
this->ui_.userIDLabel->setProperty("copy-text",
|
|
QString(TEXT_UNAVAILABLE));
|
|
};
|
|
const auto onUserFetched = [this, hack,
|
|
currentUser](const HelixUser &user) {
|
|
if (!hack.lock())
|
|
{
|
|
return;
|
|
}
|
|
|
|
this->userId_ = user.id;
|
|
this->avatarUrl_ = user.profileImageUrl;
|
|
|
|
// copyable button for login name of users with a localized username
|
|
if (user.displayName.toLower() != user.login)
|
|
{
|
|
this->ui_.localizedNameLabel->setText(user.displayName);
|
|
this->ui_.localizedNameLabel->setProperty("copy-text",
|
|
user.displayName);
|
|
this->ui_.localizedNameLabel->setVisible(true);
|
|
this->ui_.localizedNameCopyButton->setVisible(true);
|
|
}
|
|
else
|
|
{
|
|
this->ui_.nameLabel->setText(user.displayName);
|
|
this->ui_.nameLabel->setProperty("copy-text", user.displayName);
|
|
}
|
|
|
|
this->setWindowTitle(TEXT_TITLE.arg(
|
|
user.displayName, this->underlyingChannel_->getName()));
|
|
this->ui_.createdDateLabel->setText(
|
|
TEXT_CREATED.arg(user.createdAt.section("T", 0, 0)));
|
|
this->ui_.userIDLabel->setText(TEXT_USER_ID + user.id);
|
|
this->ui_.userIDLabel->setProperty("copy-text", user.id);
|
|
|
|
if (isInStreamerMode() &&
|
|
getSettings()->streamerModeHideUsercardAvatars)
|
|
{
|
|
this->ui_.avatarButton->setPixmap(getResources().streamerMode);
|
|
}
|
|
else
|
|
{
|
|
this->loadAvatar(user.profileImageUrl);
|
|
}
|
|
|
|
getHelix()->getUserFollowers(
|
|
user.id,
|
|
[this, hack](const auto &followers) {
|
|
if (!hack.lock())
|
|
{
|
|
return;
|
|
}
|
|
this->ui_.followerCountLabel->setText(
|
|
TEXT_FOLLOWERS.arg(localizeNumbers(followers.total)));
|
|
},
|
|
[] {
|
|
// on failure
|
|
});
|
|
|
|
// get ignore state
|
|
bool isIgnoring = false;
|
|
|
|
if (auto blocks = currentUser->accessBlockedUserIds();
|
|
blocks->find(user.id) != blocks->end())
|
|
{
|
|
isIgnoring = true;
|
|
}
|
|
|
|
// get ignoreHighlights state
|
|
bool isIgnoringHighlights = false;
|
|
const auto &vector = getSettings()->blacklistedUsers.raw();
|
|
for (int i = 0; i < vector.size(); i++)
|
|
{
|
|
if (this->userName_ == vector[i].getPattern())
|
|
{
|
|
isIgnoringHighlights = true;
|
|
break;
|
|
}
|
|
}
|
|
if (getSettings()->isBlacklistedUser(this->userName_) &&
|
|
!isIgnoringHighlights)
|
|
{
|
|
this->ui_.ignoreHighlights->setToolTip("Name matched by regex");
|
|
}
|
|
else
|
|
{
|
|
this->ui_.ignoreHighlights->setEnabled(true);
|
|
}
|
|
this->ui_.block->setChecked(isIgnoring);
|
|
this->ui_.block->setEnabled(true);
|
|
this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights);
|
|
|
|
// get followage and subage
|
|
getIvr()->getSubage(
|
|
this->userName_, this->underlyingChannel_->getName(),
|
|
[this, hack](const IvrSubage &subageInfo) {
|
|
if (!hack.lock())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!subageInfo.followingSince.isEmpty())
|
|
{
|
|
QDateTime followedAt = QDateTime::fromString(
|
|
subageInfo.followingSince, Qt::ISODate);
|
|
QString followingSince = followedAt.toString("yyyy-MM-dd");
|
|
this->ui_.followageLabel->setText("❤ Following since " +
|
|
followingSince);
|
|
}
|
|
|
|
if (subageInfo.isSubHidden)
|
|
{
|
|
this->ui_.subageLabel->setText(
|
|
"Subscription status hidden");
|
|
}
|
|
else if (subageInfo.isSubbed)
|
|
{
|
|
this->ui_.subageLabel->setText(
|
|
QString("★ Tier %1 - Subscribed for %2 months")
|
|
.arg(subageInfo.subTier)
|
|
.arg(subageInfo.totalSubMonths));
|
|
}
|
|
else if (subageInfo.totalSubMonths)
|
|
{
|
|
this->ui_.subageLabel->setText(
|
|
QString("★ Previously subscribed for %1 months")
|
|
.arg(subageInfo.totalSubMonths));
|
|
}
|
|
},
|
|
[] {});
|
|
};
|
|
|
|
getHelix()->getUserByName(this->userName_, onUserFetched,
|
|
onUserFetchFailed);
|
|
|
|
this->ui_.block->setEnabled(false);
|
|
this->ui_.ignoreHighlights->setEnabled(false);
|
|
}
|
|
|
|
void UserInfoPopup::loadAvatar(const QUrl &url)
|
|
{
|
|
QNetworkRequest req(url);
|
|
static auto manager = new QNetworkAccessManager();
|
|
auto *reply = manager->get(req);
|
|
|
|
QObject::connect(reply, &QNetworkReply::finished, this, [=, this] {
|
|
if (reply->error() == QNetworkReply::NoError)
|
|
{
|
|
const auto data = reply->readAll();
|
|
|
|
// might want to cache the avatar image
|
|
QPixmap avatar;
|
|
avatar.loadFromData(data);
|
|
this->ui_.avatarButton->setPixmap(avatar);
|
|
}
|
|
else
|
|
{
|
|
this->ui_.avatarButton->setPixmap(QPixmap());
|
|
}
|
|
});
|
|
}
|
|
|
|
//
|
|
// TimeoutWidget
|
|
//
|
|
UserInfoPopup::TimeoutWidget::TimeoutWidget()
|
|
: BaseWidget(nullptr)
|
|
{
|
|
auto layout = LayoutCreator<TimeoutWidget>(this)
|
|
.setLayoutType<QHBoxLayout>()
|
|
.withoutMargin();
|
|
|
|
QColor color1(255, 255, 255, 80);
|
|
QColor color2(255, 255, 255, 0);
|
|
|
|
int buttonWidth = 40;
|
|
// int buttonWidth = 24;
|
|
int buttonWidth2 = 32;
|
|
int buttonHeight = 32;
|
|
|
|
layout->setSpacing(16);
|
|
|
|
//auto addButton = [&](Action action, const QString &text,
|
|
// const QPixmap &pixmap) {
|
|
// auto vbox = layout.emplace<QVBoxLayout>().withoutMargin();
|
|
// {
|
|
// auto title = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
// title->addStretch(1);
|
|
// auto label = title.emplace<Label>(text);
|
|
// label->setHasOffset(false);
|
|
// label->setStyleSheet("color: #BBB");
|
|
// title->addStretch(1);
|
|
|
|
// auto hbox = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
// hbox->setSpacing(0);
|
|
// {
|
|
// auto button = hbox.emplace<Button>(nullptr);
|
|
// button->setPixmap(pixmap);
|
|
// button->setScaleIndependantSize(buttonHeight, buttonHeight);
|
|
// button->setBorderColor(QColor(255, 255, 255, 127));
|
|
|
|
// QObject::connect(
|
|
// button.getElement(), &Button::leftClicked, [this, action] {
|
|
// this->buttonClicked.invoke(std::make_pair(action, -1));
|
|
// });
|
|
// }
|
|
// }
|
|
//};
|
|
|
|
const auto addLayout = [&](const QString &text) {
|
|
auto vbox = layout.emplace<QVBoxLayout>().withoutMargin();
|
|
auto title = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
title->addStretch(1);
|
|
auto label = title.emplace<Label>(text);
|
|
label->setStyleSheet("color: #BBB");
|
|
label->setHasOffset(false);
|
|
title->addStretch(1);
|
|
|
|
auto hbox = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
hbox->setSpacing(0);
|
|
return hbox;
|
|
};
|
|
|
|
const auto addButton = [&](Action action, const QString &title,
|
|
const QPixmap &pixmap) {
|
|
auto button = addLayout(title).emplace<Button>(nullptr);
|
|
button->setPixmap(pixmap);
|
|
button->setScaleIndependantSize(buttonHeight, buttonHeight);
|
|
button->setBorderColor(QColor(255, 255, 255, 127));
|
|
|
|
QObject::connect(
|
|
button.getElement(), &Button::leftClicked, [this, action] {
|
|
this->buttonClicked.invoke(std::make_pair(action, -1));
|
|
});
|
|
};
|
|
|
|
auto addTimeouts = [&](const QString &title) {
|
|
auto hbox = addLayout(title);
|
|
|
|
for (const auto &item : getSettings()->timeoutButtons.getValue())
|
|
{
|
|
auto a = hbox.emplace<EffectLabel2>();
|
|
a->getLabel().setText(QString::number(item.second) + item.first);
|
|
|
|
a->setScaleIndependantSize(buttonWidth, buttonHeight);
|
|
a->setBorderColor(borderColor);
|
|
|
|
const auto pair =
|
|
std::make_pair(Action::Timeout, calculateTimeoutDuration(item));
|
|
|
|
QObject::connect(a.getElement(), &EffectLabel2::leftClicked,
|
|
[this, pair] {
|
|
this->buttonClicked.invoke(pair);
|
|
});
|
|
|
|
//auto addTimeouts = [&](const QString &title_,
|
|
// const std::vector<std::pair<QString, int>> &items) {
|
|
// auto vbox = layout.emplace<QVBoxLayout>().withoutMargin();
|
|
// {
|
|
// auto title = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
// title->addStretch(1);
|
|
// auto label = title.emplace<Label>(title_);
|
|
// label->setStyleSheet("color: #BBB");
|
|
// label->setHasOffset(false);
|
|
// title->addStretch(1);
|
|
|
|
// auto hbox = vbox.emplace<QHBoxLayout>().withoutMargin();
|
|
// hbox->setSpacing(0);
|
|
|
|
// for (const auto &item : items)
|
|
// {
|
|
// auto a = hbox.emplace<EffectLabel2>();
|
|
// a->getLabel().setText(std::get<0>(item));
|
|
|
|
// if (std::get<0>(item).length() > 1)
|
|
// {
|
|
// a->setScaleIndependantSize(buttonWidth2, buttonHeight);
|
|
// }
|
|
// else
|
|
// {
|
|
// a->setScaleIndependantSize(buttonWidth, buttonHeight);
|
|
// }
|
|
// a->setBorderColor(color1);
|
|
|
|
// QObject::connect(a.getElement(), &EffectLabel2::leftClicked,
|
|
// [this, timeout = std::get<1>(item)] {
|
|
// this->buttonClicked.invoke(std::make_pair(
|
|
// Action::Timeout, timeout));
|
|
// });
|
|
// }
|
|
}
|
|
};
|
|
|
|
addButton(Unban, "Unban", getResources().buttons.unban);
|
|
addTimeouts("Timeouts");
|
|
addButton(Ban, "Ban", getResources().buttons.ban);
|
|
}
|
|
|
|
void UserInfoPopup::TimeoutWidget::paintEvent(QPaintEvent *)
|
|
{
|
|
// QPainter painter(this);
|
|
|
|
// painter.setPen(QColor(255, 255, 255, 63));
|
|
|
|
// painter.drawLine(0, this->height() / 2, this->width(), this->height()
|
|
// / 2);
|
|
}
|
|
|
|
} // namespace chatterino
|