mirror-chatterino2/src/widgets/dialogs/UserInfoPopup.cpp
pajlada d2f1516818
Fix crash that could occur if closing the usercard quickly after blocking (#4711)
* 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)
2023-07-01 12:01:47 +00:00

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