refactor: some Application & style things (#5561)

This commit is contained in:
pajlada 2024-08-25 15:33:07 +02:00 committed by GitHub
parent ac88730563
commit 627c735524
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 733 additions and 678 deletions

View file

@ -73,3 +73,6 @@ CheckOptions:
- key: misc-const-correctness.AnalyzeValues - key: misc-const-correctness.AnalyzeValues
value: false value: false
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: true

View file

@ -20,6 +20,7 @@ class BaseApplication : public EmptyApplication
public: public:
BaseApplication() BaseApplication()
: settings(this->args, this->settingsDir.path()) : settings(this->args, this->settingsDir.path())
, updates(this->paths_, this->settings)
, theme(this->paths_) , theme(this->paths_)
, fonts(this->settings) , fonts(this->settings)
{ {
@ -28,11 +29,17 @@ public:
explicit BaseApplication(const QString &settingsData) explicit BaseApplication(const QString &settingsData)
: EmptyApplication(settingsData) : EmptyApplication(settingsData)
, settings(this->args, this->settingsDir.path()) , settings(this->args, this->settingsDir.path())
, updates(this->paths_, this->settings)
, theme(this->paths_) , theme(this->paths_)
, fonts(this->settings) , fonts(this->settings)
{ {
} }
Updates &getUpdates() override
{
return this->updates;
}
IStreamerMode *getStreamerMode() override IStreamerMode *getStreamerMode() override
{ {
return &this->streamerMode; return &this->streamerMode;
@ -60,6 +67,7 @@ public:
Args args; Args args;
Settings settings; Settings settings;
Updates updates;
DisabledStreamerMode streamerMode; DisabledStreamerMode streamerMode;
Theme theme; Theme theme;
Fonts fonts; Fonts fonts;

View file

@ -12,13 +12,9 @@ namespace chatterino::mock {
class EmptyApplication : public IApplication class EmptyApplication : public IApplication
{ {
public: public:
EmptyApplication() EmptyApplication() = default;
: updates_(this->paths_)
{
}
explicit EmptyApplication(const QString &settingsData) explicit EmptyApplication(const QString &settingsData)
: EmptyApplication()
{ {
QFile settingsFile(this->settingsDir.filePath("settings.json")); QFile settingsFile(this->settingsDir.filePath("settings.json"));
settingsFile.open(QIODevice::WriteOnly | QIODevice::Text); settingsFile.open(QIODevice::WriteOnly | QIODevice::Text);
@ -212,11 +208,6 @@ public:
} }
#endif #endif
Updates &getUpdates() override
{
return this->updates_;
}
BttvEmotes *getBttvEmotes() override BttvEmotes *getBttvEmotes() override
{ {
assert(false && "EmptyApplication::getBttvEmotes was called without " assert(false && "EmptyApplication::getBttvEmotes was called without "
@ -269,7 +260,6 @@ public:
QTemporaryDir settingsDir; QTemporaryDir settingsDir;
Paths paths_; Paths paths_;
Args args_; Args args_;
Updates updates_;
}; };
} // namespace chatterino::mock } // namespace chatterino::mock

View file

@ -61,10 +61,9 @@
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include <miniaudio.h> #include <miniaudio.h>
#include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <atomic>
namespace { namespace {
using namespace chatterino; using namespace chatterino;
@ -128,8 +127,6 @@ IApplication *INSTANCE = nullptr;
namespace chatterino { namespace chatterino {
static std::atomic<bool> isAppInitialized{false};
IApplication::IApplication() IApplication::IApplication()
{ {
INSTANCE = this; INSTANCE = this;
@ -194,8 +191,7 @@ Application::~Application()
void Application::initialize(Settings &settings, const Paths &paths) void Application::initialize(Settings &settings, const Paths &paths)
{ {
assert(isAppInitialized == false); assert(!this->initialized);
isAppInitialized = true;
// Show changelog // Show changelog
if (!this->args_.isFramelessEmbed && if (!this->args_.isFramelessEmbed &&
@ -271,17 +267,19 @@ void Application::initialize(Settings &settings, const Paths &paths)
{ {
this->initNm(paths); this->initNm(paths);
} }
this->initPubSub(); this->twitchPubSub->initialize();
this->initBttvLiveUpdates(); this->initBttvLiveUpdates();
this->initSeventvEventAPI(); this->initSeventvEventAPI();
this->streamerMode->start(); this->streamerMode->start();
this->initialized = true;
} }
int Application::run(QApplication &qtApp) int Application::run()
{ {
assert(isAppInitialized); assert(this->initialized);
this->twitch->connect(); this->twitch->connect();
@ -290,44 +288,23 @@ int Application::run(QApplication &qtApp)
this->windows->getMainWindow().show(); this->windows->getMainWindow().show();
} }
getSettings()->betaUpdates.connect(
[this] {
this->updates.checkForUpdates();
},
false);
getSettings()->enableBTTVGlobalEmotes.connect(
[this] {
this->bttvEmotes->loadEmotes();
},
false);
getSettings()->enableBTTVChannelEmotes.connect( getSettings()->enableBTTVChannelEmotes.connect(
[this] { [this] {
this->twitch->reloadAllBTTVChannelEmotes(); this->twitch->reloadAllBTTVChannelEmotes();
}, },
false); false);
getSettings()->enableFFZGlobalEmotes.connect(
[this] {
this->ffzEmotes->loadEmotes();
},
false);
getSettings()->enableFFZChannelEmotes.connect( getSettings()->enableFFZChannelEmotes.connect(
[this] { [this] {
this->twitch->reloadAllFFZChannelEmotes(); this->twitch->reloadAllFFZChannelEmotes();
}, },
false); false);
getSettings()->enableSevenTVGlobalEmotes.connect(
[this] {
this->seventvEmotes->loadGlobalEmotes();
},
false);
getSettings()->enableSevenTVChannelEmotes.connect( getSettings()->enableSevenTVChannelEmotes.connect(
[this] { [this] {
this->twitch->reloadAllSevenTVChannelEmotes(); this->twitch->reloadAllSevenTVChannelEmotes();
}, },
false); false);
return qtApp.exec(); return QApplication::exec();
} }
Theme *Application::getThemes() Theme *Application::getThemes()
@ -597,455 +574,6 @@ void Application::initNm(const Paths &paths)
#endif #endif
} }
void Application::initPubSub()
{
// We can safely ignore these signal connections since the twitch object will always
// be destroyed before the Application
std::ignore = this->twitchPubSub->moderation.chatCleared.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text =
QString("%1 cleared the chat.").arg(action.source.login);
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
std::ignore = this->twitchPubSub->moderation.modeChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text =
QString("%1 turned %2 %3 mode.")
.arg(action.source.login)
.arg(action.state == ModeChangedAction::State::On ? "on"
: "off")
.arg(action.getModeName());
if (action.duration > 0)
{
text += QString(" (%1 seconds)").arg(action.duration);
}
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
std::ignore = this->twitchPubSub->moderation.moderationStateChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text;
text = QString("%1 %2 %3.")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
std::ignore = this->twitchPubSub->moderation.userBanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
MessageBuilder msg(action);
msg->flags.set(MessageFlag::PubSub);
chan->addOrReplaceTimeout(msg.release());
});
});
std::ignore = this->twitchPubSub->moderation.userWarned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
// TODO: Resolve the moderator's user ID into a full user here, so message can look better
postToThread([chan, action] {
MessageBuilder msg(action);
msg->flags.set(MessageFlag::PubSub);
chan->addMessage(msg.release(), MessageContext::Original);
});
});
std::ignore = this->twitchPubSub->moderation.messageDeleted.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty() || getSettings()->hideDeletionActions)
{
return;
}
auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action);
postToThread([chan, msg] {
auto replaced = false;
LimitedQueueSnapshot<MessagePtr> snapshot =
chan->getMessageSnapshot();
int snapshotLength = snapshot.size();
// without parens it doesn't build on windows
int end = (std::max)(0, snapshotLength - 200);
for (int i = snapshotLength - 1; i >= end; --i)
{
const auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
chan->replaceMessage(s, msg);
replaced = true;
break;
}
}
if (!replaced)
{
chan->addMessage(msg, MessageContext::Original);
}
});
});
std::ignore = this->twitchPubSub->moderation.userUnbanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
std::ignore =
this->twitchPubSub->moderation.suspiciousMessageReceived.connect(
[&](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious message with unknown "
"treatment:"
<< action.treatmentString;
return;
}
// monitored chats are received over irc; in the future, we will use pubsub instead
if (action.treatment !=
PubSubLowTrustUsersMessage::Treatment::Restricted)
{
return;
}
if (getSettings()->streamerModeHideModActions &&
this->getStreamerMode()->isEnabled())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
auto twitchChannel =
std::dynamic_pointer_cast<TwitchChannel>(chan);
if (!twitchChannel)
{
return;
}
postToThread([twitchChannel, action] {
const auto p = MessageBuilder::makeLowTrustUserMessage(
action, twitchChannel->getName(), twitchChannel.get());
twitchChannel->addMessage(p.first,
MessageContext::Original);
twitchChannel->addMessage(p.second,
MessageContext::Original);
});
});
std::ignore =
this->twitchPubSub->moderation.suspiciousTreatmentUpdated.connect(
[&](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious user update with unknown "
"treatment:"
<< action.treatmentString;
return;
}
if (action.updatedByUserLogin.isEmpty())
{
return;
}
if (getSettings()->streamerModeHideModActions &&
this->getStreamerMode()->isEnabled())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
auto msg =
MessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg, MessageContext::Original);
});
});
std::ignore = this->twitchPubSub->moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &channelID) {
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
{
return;
}
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
action.reason = QString("%1 level %2")
.arg(msg.contentCategory)
.arg(msg.contentLevel);
action.msgID = msg.messageID;
action.message = msg.messageText;
// this message also contains per-word automod data, which could be implemented
// extract sender data manually because Twitch loves not being consistent
QString senderDisplayName =
msg.senderUserDisplayName; // Might be transformed later
bool hasLocalizedName = false;
if (!msg.senderUserDisplayName.isEmpty())
{
// check for non-ascii display names
if (QString::compare(msg.senderUserDisplayName,
msg.senderUserLogin,
Qt::CaseInsensitive) != 0)
{
hasLocalizedName = true;
}
}
QColor senderColor = msg.senderUserChatColor;
QString senderColor_;
if (!senderColor.isValid() &&
getSettings()->colorizeNicknames)
{
// color may be not present if user is a grey-name
senderColor = getRandomColor(msg.senderUserID);
}
// handle username style based on prefered setting
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
{
senderDisplayName = msg.senderUserLogin;
}
break;
}
case UsernameDisplayMode::LocalizedName: {
break;
}
case UsernameDisplayMode::
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName = QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
break;
}
}
action.target =
ActionUser{msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second,
MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.first, MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.second,
MessageContext::Original);
if (getSettings()->showAutomodInMentions)
{
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.first,
MessageContext::Original);
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.second,
MessageContext::Original);
}
});
}
// "ALLOWED" and "DENIED" statuses remain unimplemented
// They are versions of automod_message_(denied|approved) but for mods.
}
break;
case PubSubAutoModQueueMessage::Type::INVALID:
default: {
}
break;
}
});
std::ignore = this->twitchPubSub->moderation.autoModMessageBlocked.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p =
MessageBuilder::makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second, MessageContext::Original);
});
});
std::ignore = this->twitchPubSub->moderation.automodUserMessage.connect(
[&](const auto &action) {
if (getSettings()->streamerModeHideModActions &&
this->getStreamerMode()->isEnabled())
{
return;
}
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
chan->deleteMessage(msg->id);
});
std::ignore = this->twitchPubSub->moderation.automodInfoMessage.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodInfoMessage(action);
chan->addMessage(p, MessageContext::Original);
});
});
std::ignore =
this->twitchPubSub->pointReward.redeemed.connect([&](auto &data) {
QString channelId = data.value("channel_id").toString();
if (channelId.isEmpty())
{
qCDebug(chatterinoApp)
<< "Couldn't find channel id of point reward";
return;
}
auto chan = this->twitch->getChannelOrEmptyByID(channelId);
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
});
});
this->twitchPubSub->start();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
this->accounts->twitch.currentUserChanged.connect(
[this] {
this->twitchPubSub->unlistenChannelModerationActions();
this->twitchPubSub->unlistenAutomod();
this->twitchPubSub->unlistenLowTrustUsers();
this->twitchPubSub->unlistenChannelPointRewards();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
},
boost::signals2::at_front);
}
void Application::initBttvLiveUpdates() void Application::initBttvLiveUpdates()
{ {
if (!this->bttvLiveUpdates) if (!this->bttvLiveUpdates)

View file

@ -2,8 +2,6 @@
#include "singletons/NativeMessaging.hpp" #include "singletons/NativeMessaging.hpp"
#include <QApplication>
#include <cassert> #include <cassert>
#include <memory> #include <memory>
@ -133,7 +131,7 @@ public:
void load(); void load();
void save(); void save();
int run(QApplication &qtApp); int run();
friend void test(); friend void test();
@ -219,13 +217,14 @@ public:
IStreamerMode *getStreamerMode() override; IStreamerMode *getStreamerMode() override;
private: private:
void initPubSub();
void initBttvLiveUpdates(); void initBttvLiveUpdates();
void initSeventvEventAPI(); void initSeventvEventAPI();
void initNm(const Paths &paths); void initNm(const Paths &paths);
NativeMessagingServer nmServer; NativeMessagingServer nmServer;
Updates &updates; Updates &updates;
bool initialized{false};
}; };
IApplication *getApp(); IApplication *getApp();

View file

@ -41,7 +41,7 @@ namespace {
{ {
// borrowed from // borrowed from
// https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows
auto dark = qApp->palette(); auto dark = QApplication::palette();
dark.setColor(QPalette::Window, QColor(22, 22, 22)); dark.setColor(QPalette::Window, QColor(22, 22, 22));
dark.setColor(QPalette::WindowText, Qt::white); dark.setColor(QPalette::WindowText, Qt::white);
@ -71,7 +71,7 @@ namespace {
dark.setColor(QPalette::Disabled, QPalette::WindowText, dark.setColor(QPalette::Disabled, QPalette::WindowText,
QColor(127, 127, 127)); QColor(127, 127, 127));
qApp->setPalette(dark); QApplication::setPalette(dark);
} }
void initQt() void initQt()
@ -263,7 +263,7 @@ void runGui(QApplication &a, const Paths &paths, Settings &settings,
Application app(settings, paths, args, updates); Application app(settings, paths, args, updates);
app.initialize(settings, paths); app.initialize(settings, paths);
app.run(a); app.run();
app.save(); app.save();
settings.requestSave(); settings.requestSave();

View file

@ -2,12 +2,10 @@
#include "debug/Benchmark.hpp" #include "debug/Benchmark.hpp"
#include <tuple>
namespace chatterino { namespace chatterino {
ChatterSet::ChatterSet() ChatterSet::ChatterSet()
: items(chatterLimit) : items(ChatterSet::CHATTER_LIMIT)
{ {
} }
@ -22,7 +20,7 @@ void ChatterSet::updateOnlineChatters(
BenchmarkGuard bench("update online chatters"); BenchmarkGuard bench("update online chatters");
// Create a new lru cache without the users that are not present anymore. // Create a new lru cache without the users that are not present anymore.
cache::lru_cache<QString, QString> tmp(chatterLimit); cache::lru_cache<QString, QString> tmp(ChatterSet::CHATTER_LIMIT);
for (auto &&chatter : lowerCaseUsernames) for (auto &&chatter : lowerCaseUsernames)
{ {
@ -32,7 +30,7 @@ void ChatterSet::updateOnlineChatters(
// Less chatters than the limit => try to preserve as many as possible. // Less chatters than the limit => try to preserve as many as possible.
} }
else if (lowerCaseUsernames.size() < chatterLimit) else if (lowerCaseUsernames.size() < ChatterSet::CHATTER_LIMIT)
{ {
tmp.put(chatter, chatter); tmp.put(chatter, chatter);
} }

View file

@ -5,9 +5,6 @@
#include <lrucache/lrucache.hpp> #include <lrucache/lrucache.hpp>
#include <QString> #include <QString>
#include <functional>
#include <set>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
@ -19,7 +16,7 @@ class ChatterSet
{ {
public: public:
/// The limit of how many chatters can be saved for a channel. /// The limit of how many chatters can be saved for a channel.
static constexpr size_t chatterLimit = 2000; static constexpr size_t CHATTER_LIMIT = 2000;
ChatterSet(); ChatterSet();

View file

@ -8,6 +8,7 @@
#include "util/CombinePath.hpp" #include "util/CombinePath.hpp"
#include "util/Variant.hpp" #include "util/Variant.hpp"
#include <QApplication>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QSaveFile> #include <QSaveFile>
@ -89,7 +90,7 @@ void queueInsecureSave()
if (!isQueued) if (!isQueued)
{ {
isQueued = true; isQueued = true;
QTimer::singleShot(200, qApp, [] { QTimer::singleShot(200, QApplication::instance(), [] {
storeInsecure(insecureInstance()); storeInsecure(insecureInstance());
isQueued = false; isQueued = false;
}); });
@ -133,8 +134,8 @@ void runNextJob()
job->setAutoDelete(true); job->setAutoDelete(true);
job->setKey(set.name); job->setKey(set.name);
job->setTextData(set.credential); job->setTextData(set.credential);
QObject::connect(job, &QKeychain::Job::finished, qApp, QObject::connect(job, &QKeychain::Job::finished,
[](auto) { QApplication::instance(), [](auto) {
runNextJob(); runNextJob();
}); });
job->start(); job->start();
@ -143,8 +144,8 @@ void runNextJob()
auto *job = new QKeychain::DeletePasswordJob("chatterino"); auto *job = new QKeychain::DeletePasswordJob("chatterino");
job->setAutoDelete(true); job->setAutoDelete(true);
job->setKey(erase.name); job->setKey(erase.name);
QObject::connect(job, &QKeychain::Job::finished, qApp, QObject::connect(job, &QKeychain::Job::finished,
[](auto) { QApplication::instance(), [](auto) {
runNextJob(); runNextJob();
}); });
job->start(); job->start();

View file

@ -4,6 +4,7 @@
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "common/Version.hpp" #include "common/Version.hpp"
#include <QApplication>
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include <QtConcurrent> #include <QtConcurrent>
@ -44,7 +45,7 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) &&
if (caller) if (caller)
{ {
// Caller must be in gui thread // Caller must be in gui thread
assert(caller->thread() == qApp->thread()); assert(caller->thread() == QApplication::instance()->thread());
this->data->caller = const_cast<QObject *>(caller); this->data->caller = const_cast<QObject *>(caller);
this->data->hasCaller = true; this->data->hasCaller = true;

View file

@ -110,15 +110,15 @@ int main(int argc, char **argv)
<< QSslSocket::supportedProtocols(); << QSslSocket::supportedProtocols();
#endif #endif
Updates updates(*paths); Settings settings(args, paths->settingsDirectory);
Updates updates(*paths, settings);
NetworkConfigurationProvider::applyFromEnv(Env::get()); NetworkConfigurationProvider::applyFromEnv(Env::get());
IvrApi::initialize(); IvrApi::initialize();
Helix::initialize(); Helix::initialize();
Settings settings(args, paths->settingsDirectory);
runGui(a, *paths, settings, args, updates); runGui(a, *paths, settings, args, updates);
} }
return 0; return 0;

View file

@ -44,6 +44,7 @@
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include <boost/variant.hpp> #include <boost/variant.hpp>
#include <QApplication>
#include <QColor> #include <QColor>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>

View file

@ -15,82 +15,57 @@
#include <QJsonArray> #include <QJsonArray>
#include <QThread> #include <QThread>
namespace chatterino {
namespace { namespace {
const QString CHANNEL_HAS_NO_EMOTES( using namespace chatterino;
"This channel has no BetterTTV channel emotes.");
QString emoteLinkFormat("https://betterttv.com/emotes/%1"); const QString CHANNEL_HAS_NO_EMOTES(
// BTTV doesn't provide any data on the size, so we assume an emote is 28x28 "This channel has no BetterTTV channel emotes.");
constexpr QSize EMOTE_BASE_SIZE(28, 28);
struct CreateEmoteResult { /// The emote page template.
EmoteId id; ///
EmoteName name; /// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a)
Emote emote; constexpr QStringView EMOTE_LINK_FORMAT = u"https://betterttv.com/emotes/%1";
};
Url getEmoteLink(QString urlTemplate, const EmoteId &id, /// The emote CDN link template.
const QString &emoteScale) ///
/// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a)
///
/// %2 being the emote size (e.g. 3x)
constexpr QStringView EMOTE_CDN_FORMAT =
u"https://cdn.betterttv.net/emote/%1/%2";
// BTTV doesn't provide any data on the size, so we assume an emote is 28x28
constexpr QSize EMOTE_BASE_SIZE(28, 28);
struct CreateEmoteResult {
EmoteId id;
EmoteName name;
Emote emote;
};
Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale)
{
return {EMOTE_CDN_FORMAT.arg(id.string, emoteScale)};
}
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseGlobalEmotes(const QJsonArray &jsonEmotes,
const EmoteMap &currentEmotes)
{
auto emotes = EmoteMap();
for (auto jsonEmote : jsonEmotes)
{ {
urlTemplate.detach(); auto id = EmoteId{jsonEmote.toObject().value("id").toString()};
auto name = EmoteName{jsonEmote.toObject().value("code").toString()};
return {urlTemplate.replace("{{id}}", id.string)
.replace("{{image}}", emoteScale)};
}
Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale)
{
static const QString urlTemplate(
"https://cdn.betterttv.net/emote/%1/%2");
return {urlTemplate.arg(id.string, emoteScale)};
}
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseGlobalEmotes(
const QJsonArray &jsonEmotes, const EmoteMap &currentEmotes)
{
auto emotes = EmoteMap();
for (auto jsonEmote : jsonEmotes)
{
auto id = EmoteId{jsonEmote.toObject().value("id").toString()};
auto name =
EmoteName{jsonEmote.toObject().value("code").toString()};
auto emote = Emote({
name,
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1,
EMOTE_BASE_SIZE),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5,
EMOTE_BASE_SIZE * 2),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25,
EMOTE_BASE_SIZE * 4)},
Tooltip{name.string + "<br>Global BetterTTV Emote"},
Url{emoteLinkFormat.arg(id.string)},
});
emotes[name] =
cachedOrMakeEmotePtr(std::move(emote), currentEmotes);
}
return {Success, std::move(emotes)};
}
CreateEmoteResult createChannelEmote(const QString &channelDisplayName,
const QJsonObject &jsonEmote)
{
auto id = EmoteId{jsonEmote.value("id").toString()};
auto name = EmoteName{jsonEmote.value("code").toString()};
auto author = EmoteAuthor{
jsonEmote.value("user").toObject().value("displayName").toString()};
auto emote = Emote({ auto emote = Emote({
name, name,
@ -99,58 +74,82 @@ namespace {
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5,
EMOTE_BASE_SIZE * 2), EMOTE_BASE_SIZE * 2),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25,
EMOTE_BASE_SIZE * 4), EMOTE_BASE_SIZE * 4)},
}, Tooltip{name.string + "<br>Global BetterTTV Emote"},
Tooltip{ Url{EMOTE_LINK_FORMAT.arg(id.string)},
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
.arg(name.string)
// when author is empty, it is a channel emote created by the broadcaster
.arg(author.string.isEmpty() ? "Channel" : "Shared")
.arg(author.string.isEmpty() ? channelDisplayName
: author.string)},
Url{emoteLinkFormat.arg(id.string)},
false,
id,
}); });
return {id, name, emote}; emotes[name] = cachedOrMakeEmotePtr(std::move(emote), currentEmotes);
} }
bool updateChannelEmote(Emote &emote, const QString &channelDisplayName, return {Success, std::move(emotes)};
const QJsonObject &jsonEmote) }
CreateEmoteResult createChannelEmote(const QString &channelDisplayName,
const QJsonObject &jsonEmote)
{
auto id = EmoteId{jsonEmote.value("id").toString()};
auto name = EmoteName{jsonEmote.value("code").toString()};
auto author = EmoteAuthor{
jsonEmote.value("user").toObject().value("displayName").toString()};
auto emote = Emote({
name,
ImageSet{
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, EMOTE_BASE_SIZE),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, EMOTE_BASE_SIZE * 2),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, EMOTE_BASE_SIZE * 4),
},
Tooltip{
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
.arg(name.string)
// when author is empty, it is a channel emote created by the broadcaster
.arg(author.string.isEmpty() ? "Channel" : "Shared")
.arg(author.string.isEmpty() ? channelDisplayName
: author.string)},
Url{EMOTE_LINK_FORMAT.arg(id.string)},
false,
id,
});
return {id, name, emote};
}
bool updateChannelEmote(Emote &emote, const QString &channelDisplayName,
const QJsonObject &jsonEmote)
{
bool anyModifications = false;
if (jsonEmote.contains("code"))
{ {
bool anyModifications = false; emote.name = EmoteName{jsonEmote.value("code").toString()};
anyModifications = true;
if (jsonEmote.contains("code"))
{
emote.name = EmoteName{jsonEmote.value("code").toString()};
anyModifications = true;
}
if (jsonEmote.contains("user"))
{
emote.author = EmoteAuthor{jsonEmote.value("user")
.toObject()
.value("displayName")
.toString()};
anyModifications = true;
}
if (anyModifications)
{
emote.tooltip = Tooltip{
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
.arg(emote.name.string)
// when author is empty, it is a channel emote created by the broadcaster
.arg(emote.author.string.isEmpty() ? "Channel" : "Shared")
.arg(emote.author.string.isEmpty() ? channelDisplayName
: emote.author.string)};
}
return anyModifications;
} }
if (jsonEmote.contains("user"))
{
emote.author = EmoteAuthor{
jsonEmote.value("user").toObject().value("displayName").toString()};
anyModifications = true;
}
if (anyModifications)
{
emote.tooltip = Tooltip{
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
.arg(emote.name.string)
// when author is empty, it is a channel emote created by the broadcaster
.arg(emote.author.string.isEmpty() ? "Channel" : "Shared")
.arg(emote.author.string.isEmpty() ? channelDisplayName
: emote.author.string)};
}
return anyModifications;
}
} // namespace } // namespace
namespace chatterino {
using namespace bttv::detail; using namespace bttv::detail;
EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot, EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot,
@ -182,6 +181,11 @@ EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot,
BttvEmotes::BttvEmotes() BttvEmotes::BttvEmotes()
: global_(std::make_shared<EmoteMap>()) : global_(std::make_shared<EmoteMap>())
{ {
getSettings()->enableBTTVGlobalEmotes.connect(
[this] {
this->loadEmotes();
},
this->managedConnections, false);
} }
std::shared_ptr<const EmoteMap> BttvEmotes::emotes() const std::shared_ptr<const EmoteMap> BttvEmotes::emotes() const
@ -360,15 +364,4 @@ std::optional<EmotePtr> BttvEmotes::removeEmote(
return emote; return emote;
} }
/*
static Url getEmoteLink(QString urlTemplate, const EmoteId &id,
const QString &emoteScale)
{
urlTemplate.detach();
return {urlTemplate.replace("{{id}}", id.string)
.replace("{{image}}", emoteScale)};
}
*/
} // namespace chatterino } // namespace chatterino

View file

@ -3,10 +3,15 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonObject> #include <QJsonObject>
#include <QString>
#include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <utility>
#include <vector>
namespace chatterino { namespace chatterino {
@ -81,6 +86,9 @@ public:
private: private:
Atomic<std::shared_ptr<const EmoteMap>> global_; Atomic<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include <memory> #include <memory>
#include <optional> #include <optional>

View file

@ -299,7 +299,7 @@ std::vector<boost::variant<EmotePtr, QString>> Emojis::parse(
auto result = std::vector<boost::variant<EmotePtr, QString>>(); auto result = std::vector<boost::variant<EmotePtr, QString>>();
QString::size_type lastParsedEmojiEndIndex = 0; QString::size_type lastParsedEmojiEndIndex = 0;
for (auto i = 0; i < text.length(); ++i) for (qsizetype i = 0; i < text.length(); ++i)
{ {
const QChar character = text.at(i); const QChar character = text.at(i);
@ -401,7 +401,7 @@ QString Emojis::replaceShortCodes(const QString &text) const
QString ret(text); QString ret(text);
auto it = this->findShortCodesRegex_.globalMatch(text); auto it = this->findShortCodesRegex_.globalMatch(text);
int32_t offset = 0; qsizetype offset = 0;
while (it.hasNext()) while (it.hasNext())
{ {

View file

@ -12,9 +12,6 @@
#include <QThread> #include <QThread>
#include <QUrl> #include <QUrl>
#include <map>
#include <shared_mutex>
namespace chatterino { namespace chatterino {
std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id) std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id)

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "util/QStringHash.hpp"
#include "util/ThreadGuard.hpp" #include "util/ThreadGuard.hpp"
#include <QColor> #include <QColor>

View file

@ -206,6 +206,11 @@ FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot)
FfzEmotes::FfzEmotes() FfzEmotes::FfzEmotes()
: global_(std::make_shared<EmoteMap>()) : global_(std::make_shared<EmoteMap>())
{ {
getSettings()->enableFFZGlobalEmotes.connect(
[this] {
this->loadEmotes();
},
this->managedConnections, false);
} }
std::shared_ptr<const EmoteMap> FfzEmotes::emotes() const std::shared_ptr<const EmoteMap> FfzEmotes::emotes() const

View file

@ -5,10 +5,14 @@
#include "util/QStringHash.hpp" #include "util/QStringHash.hpp"
#include <boost/unordered/unordered_flat_map.hpp> #include <boost/unordered/unordered_flat_map.hpp>
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonObject> #include <QJsonObject>
#include <QString>
#include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
namespace chatterino { namespace chatterino {
@ -51,6 +55,9 @@ public:
private: private:
Atomic<std::shared_ptr<const EmoteMap>> global_; Atomic<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -1,5 +1,7 @@
#include "providers/ffz/FfzUtil.hpp" #include "providers/ffz/FfzUtil.hpp"
#include <QUrl>
namespace chatterino { namespace chatterino {
Url parseFfzUrl(const QString &ffzUrl) Url parseFfzUrl(const QString &ffzUrl)

View file

@ -3,7 +3,6 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include <QString> #include <QString>
#include <QUrl>
namespace chatterino { namespace chatterino {

View file

@ -188,6 +188,11 @@ EmoteMap seventv::detail::parseEmotes(const QJsonArray &emoteSetEmotes,
SeventvEmotes::SeventvEmotes() SeventvEmotes::SeventvEmotes()
: global_(std::make_shared<EmoteMap>()) : global_(std::make_shared<EmoteMap>())
{ {
getSettings()->enableSevenTVGlobalEmotes.connect(
[this] {
this->loadGlobalEmotes();
},
this->managedConnections, false);
} }
std::shared_ptr<const EmoteMap> SeventvEmotes::globalEmotes() const std::shared_ptr<const EmoteMap> SeventvEmotes::globalEmotes() const

View file

@ -4,11 +4,17 @@
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include "common/FlagsEnum.hpp" #include "common/FlagsEnum.hpp"
#include <pajlada/signals/scoped-connection.hpp>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
namespace chatterino { namespace chatterino {
@ -21,7 +27,7 @@ namespace seventv::eventapi {
} // namespace seventv::eventapi } // namespace seventv::eventapi
// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36 // https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36
enum class SeventvActiveEmoteFlag : int64_t { enum class SeventvActiveEmoteFlag : std::int64_t {
None = 0LL, None = 0LL,
// Emote is zero-width // Emote is zero-width
@ -152,6 +158,9 @@ public:
private: private:
Atomic<std::shared_ptr<const EmoteMap>> global_; Atomic<std::shared_ptr<const EmoteMap>> global_;
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -1,6 +1,8 @@
#include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/PubSubManager.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "providers/NetworkConfigurationProvider.hpp" #include "providers/NetworkConfigurationProvider.hpp"
#include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubClient.hpp"
@ -508,6 +510,23 @@ PubSub::~PubSub()
this->stop(); this->stop();
} }
void PubSub::initialize()
{
this->start();
this->setAccount(getApp()->getAccounts()->twitch.getCurrent());
getApp()->getAccounts()->twitch.currentUserChanged.connect(
[this] {
this->unlistenChannelModerationActions();
this->unlistenAutomod();
this->unlistenLowTrustUsers();
this->unlistenChannelPointRewards();
this->setAccount(getApp()->getAccounts()->twitch.getCurrent());
},
boost::signals2::at_front);
}
void PubSub::setAccount(std::shared_ptr<TwitchAccount> account) void PubSub::setAccount(std::shared_ptr<TwitchAccount> account)
{ {
this->token_ = account->getOAuthToken(); this->token_ = account->getOAuthToken();

View file

@ -3,15 +3,21 @@
#include "providers/twitch/PubSubClientOptions.hpp" #include "providers/twitch/PubSubClientOptions.hpp"
#include "providers/twitch/PubSubWebsocket.hpp" #include "providers/twitch/PubSubWebsocket.hpp"
#include "util/ExponentialBackoff.hpp" #include "util/ExponentialBackoff.hpp"
#include "util/QStringHash.hpp"
#include <boost/asio/io_service.hpp>
#include <boost/asio/ssl/context.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include <websocketpp/client.hpp> #include <websocketpp/client.hpp>
#include <websocketpp/common/connection_hdl.hpp>
#include <websocketpp/common/memory.hpp>
#include <websocketpp/config/asio_client.hpp>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstdint>
#include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional> #include <optional>
@ -19,6 +25,10 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#if __has_include(<gtest/gtest_prod.h>)
# include <gtest/gtest_prod.h>
#endif
namespace chatterino { namespace chatterino {
class TwitchAccount; class TwitchAccount;
@ -85,10 +95,8 @@ public:
PubSub &operator=(const PubSub &) = delete; PubSub &operator=(const PubSub &) = delete;
PubSub &operator=(PubSub &&) = delete; PubSub &operator=(PubSub &&) = delete;
void setAccount(std::shared_ptr<TwitchAccount> account); /// Set up connections between itself & other parts of the application
void initialize();
void start();
void stop();
struct { struct {
Signal<ClearChatAction> chatCleared; Signal<ClearChatAction> chatCleared;
@ -192,6 +200,11 @@ public:
} diag; } diag;
private: private:
void setAccount(std::shared_ptr<TwitchAccount> account);
void start();
void stop();
/** /**
* Unlistens to all topics matching the prefix in all clients * Unlistens to all topics matching the prefix in all clients
*/ */
@ -250,6 +263,22 @@ private:
const PubSubClientOptions clientOptions_; const PubSubClientOptions clientOptions_;
bool stopping_{false}; bool stopping_{false};
#ifdef FRIEND_TEST
friend class FTest;
FRIEND_TEST(TwitchPubSubClient, ServerRespondsToPings);
FRIEND_TEST(TwitchPubSubClient, ServerDoesntRespondToPings);
FRIEND_TEST(TwitchPubSubClient, DisconnectedAfter1s);
FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimit);
FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep);
FRIEND_TEST(TwitchPubSubClient, ReceivedWhisper);
FRIEND_TEST(TwitchPubSubClient, ModeratorActionsUserBanned);
FRIEND_TEST(TwitchPubSubClient, MissingToken);
FRIEND_TEST(TwitchPubSubClient, WrongToken);
FRIEND_TEST(TwitchPubSubClient, CorrectToken);
FRIEND_TEST(TwitchPubSubClient, AutoModMessageHeld);
#endif
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -1320,8 +1320,6 @@ void TwitchChannel::refreshPubSub()
auto currentAccount = getApp()->getAccounts()->twitch.getCurrent(); auto currentAccount = getApp()->getAccounts()->twitch.getCurrent();
getApp()->getTwitchPubSub()->setAccount(currentAccount);
getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId); getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId);
if (this->hasModRights()) if (this->hasModRights())
{ {

View file

@ -16,9 +16,13 @@
#include "providers/seventv/SeventvEventAPI.hpp" #include "providers/seventv/SeventvEventAPI.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/StreamerMode.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
#include "util/RatelimitBucket.hpp" #include "util/RatelimitBucket.hpp"
@ -230,6 +234,441 @@ void TwitchIrcServer::initialize()
this->connect(); this->connect();
}); });
}); });
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.chatCleared,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text =
QString("%1 cleared the chat.").arg(action.source.login);
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.modeChanged,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text =
QString("%1 turned %2 %3 mode.")
.arg(action.source.login)
.arg(action.state == ModeChangedAction::State::On ? "on"
: "off")
.arg(action.getModeName());
if (action.duration > 0)
{
text += QString(" (%1 seconds)").arg(action.duration);
}
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.moderationStateChanged,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text;
text = QString("%1 %2 %3.")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userBanned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
MessageBuilder msg(action);
msg->flags.set(MessageFlag::PubSub);
chan->addOrReplaceTimeout(msg.release());
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userWarned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
// TODO: Resolve the moderator's user ID into a full user here, so message can look better
postToThread([chan, action] {
MessageBuilder msg(action);
msg->flags.set(MessageFlag::PubSub);
chan->addMessage(msg.release(), MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.messageDeleted,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty() || getSettings()->hideDeletionActions)
{
return;
}
auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action);
postToThread([chan, msg] {
auto replaced = false;
LimitedQueueSnapshot<MessagePtr> snapshot =
chan->getMessageSnapshot();
int snapshotLength = snapshot.size();
// without parens it doesn't build on windows
int end = (std::max)(0, snapshotLength - 200);
for (int i = snapshotLength - 1; i >= end; --i)
{
const auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
chan->replaceMessage(s, msg);
replaced = true;
break;
}
}
if (!replaced)
{
chan->addMessage(msg, MessageContext::Original);
}
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userUnbanned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.suspiciousMessageReceived,
[this](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious message with unknown "
"treatment:"
<< action.treatmentString;
return;
}
// monitored chats are received over irc; in the future, we will use pubsub instead
if (action.treatment !=
PubSubLowTrustUsersMessage::Treatment::Restricted)
{
return;
}
if (getSettings()->streamerModeHideModActions &&
getApp()->getStreamerMode()->isEnabled())
{
return;
}
auto chan = this->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(chan);
if (!twitchChannel)
{
return;
}
postToThread([twitchChannel, action] {
const auto p = MessageBuilder::makeLowTrustUserMessage(
action, twitchChannel->getName(), twitchChannel.get());
twitchChannel->addMessage(p.first, MessageContext::Original);
twitchChannel->addMessage(p.second, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.suspiciousTreatmentUpdated,
[this](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious user update with unknown "
"treatment:"
<< action.treatmentString;
return;
}
if (action.updatedByUserLogin.isEmpty())
{
return;
}
if (getSettings()->streamerModeHideModActions &&
getApp()->getStreamerMode()->isEnabled())
{
return;
}
auto chan = this->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
auto msg = MessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.autoModMessageCaught,
[this](const auto &msg, const QString &channelID) {
auto chan = this->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
{
return;
}
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
action.reason = QString("%1 level %2")
.arg(msg.contentCategory)
.arg(msg.contentLevel);
action.msgID = msg.messageID;
action.message = msg.messageText;
// this message also contains per-word automod data, which could be implemented
// extract sender data manually because Twitch loves not being consistent
QString senderDisplayName =
msg.senderUserDisplayName; // Might be transformed later
bool hasLocalizedName = false;
if (!msg.senderUserDisplayName.isEmpty())
{
// check for non-ascii display names
if (QString::compare(msg.senderUserDisplayName,
msg.senderUserLogin,
Qt::CaseInsensitive) != 0)
{
hasLocalizedName = true;
}
}
QColor senderColor = msg.senderUserChatColor;
QString senderColor_;
if (!senderColor.isValid() &&
getSettings()->colorizeNicknames)
{
// color may be not present if user is a grey-name
senderColor = getRandomColor(msg.senderUserID);
}
// handle username style based on prefered setting
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
{
senderDisplayName = msg.senderUserLogin;
}
break;
}
case UsernameDisplayMode::LocalizedName: {
break;
}
case UsernameDisplayMode::
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName = QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
break;
}
}
action.target =
ActionUser{msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second,
MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.first, MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.second,
MessageContext::Original);
if (getSettings()->showAutomodInMentions)
{
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.first,
MessageContext::Original);
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.second,
MessageContext::Original);
}
});
}
// "ALLOWED" and "DENIED" statuses remain unimplemented
// They are versions of automod_message_(denied|approved) but for mods.
}
break;
case PubSubAutoModQueueMessage::Type::INVALID:
default: {
}
break;
}
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.autoModMessageBlocked,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p =
MessageBuilder::makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.automodUserMessage,
[this](const auto &action) {
if (getSettings()->streamerModeHideModActions &&
getApp()->getStreamerMode()->isEnabled())
{
return;
}
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
chan->deleteMessage(msg->id);
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.automodInfoMessage,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodInfoMessage(action);
chan->addMessage(p, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->pointReward.redeemed, [this](auto &data) {
QString channelId = data.value("channel_id").toString();
if (channelId.isEmpty())
{
qCDebug(chatterinoApp)
<< "Couldn't find channel id of point reward";
return;
}
auto chan = this->getChannelOrEmptyByID(channelId);
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
});
});
} }
void TwitchIrcServer::initializeConnection(IrcConnection *connection, void TwitchIrcServer::initializeConnection(IrcConnection *connection,

View file

@ -202,8 +202,6 @@ private:
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_; std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
std::chrono::steady_clock::time_point lastErrorTimeSpeed_; std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
std::chrono::steady_clock::time_point lastErrorTimeAmount_; std::chrono::steady_clock::time_point lastErrorTimeAmount_;
pajlada::Signals::SignalHolder signalHolder_;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -16,6 +16,7 @@
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints> # include <QStyleHints>
#endif #endif
#include <QApplication>
#include <cmath> #include <cmath>
@ -301,8 +302,9 @@ Theme::Theme(const Paths &paths)
this->loadAvailableThemes(paths); this->loadAvailableThemes(paths);
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, QObject::connect(QApplication::styleHints(),
&this->lifetime_, [this] { &QStyleHints::colorSchemeChanged, &this->lifetime_,
[this] {
if (this->isSystemTheme()) if (this->isSystemTheme())
{ {
this->update(); this->update();
@ -320,7 +322,7 @@ void Theme::update()
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (this->isSystemTheme()) if (this->isSystemTheme())
{ {
switch (qApp->styleHints()->colorScheme()) switch (QApplication::styleHints()->colorScheme())
{ {
case Qt::ColorScheme::Light: case Qt::ColorScheme::Light:
return this->lightSystemThemeName; return this->lightSystemThemeName;

View file

@ -46,12 +46,18 @@ const QString CHATTERINO_OS = u"unknown"_s;
namespace chatterino { namespace chatterino {
Updates::Updates(const Paths &paths_) Updates::Updates(const Paths &paths_, Settings &settings)
: paths(paths_) : paths(paths_)
, currentVersion_(CHATTERINO_VERSION) , currentVersion_(CHATTERINO_VERSION)
, updateGuideLink_("https://chatterino.com") , updateGuideLink_("https://chatterino.com")
{ {
qCDebug(chatterinoUpdate) << "init UpdateManager"; qCDebug(chatterinoUpdate) << "init UpdateManager";
settings.betaUpdates.connect(
[this] {
this->checkForUpdates();
},
this->managedConnections, false);
} }
/// Checks if the online version is newer or older than the current version. /// Checks if the online version is newer or older than the current version.

View file

@ -1,11 +1,16 @@
#pragma once #pragma once
#include <pajlada/signals/scoped-connection.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <QString> #include <QString>
#include <memory>
#include <vector>
namespace chatterino { namespace chatterino {
class Paths; class Paths;
class Settings;
/** /**
* To check for updates, use the `checkForUpdates` method. * To check for updates, use the `checkForUpdates` method.
@ -16,7 +21,7 @@ class Updates
const Paths &paths; const Paths &paths;
public: public:
explicit Updates(const Paths &paths_); Updates(const Paths &paths_, Settings &settings);
enum Status { enum Status {
None, None,
@ -59,6 +64,9 @@ private:
QString updateGuideLink_; QString updateGuideLink_;
void setStatus_(Status status); void setStatus_(Status status);
std::vector<std::unique_ptr<pajlada::Signals::ScopedConnection>>
managedConnections;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -664,8 +664,6 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor)
{ {
assertInGuiThread(); assertInGuiThread();
auto *app = getApp();
if (descriptor.type_ == "twitch") if (descriptor.type_ == "twitch")
{ {
return getApp()->getTwitch()->getOrAddChannel(descriptor.channelName_); return getApp()->getTwitch()->getOrAddChannel(descriptor.channelName_);
@ -753,7 +751,7 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout)
// get geometry // get geometry
{ {
// out of bounds windows // out of bounds windows
auto screens = qApp->screens(); auto screens = QApplication::screens();
bool outOfBounds = bool outOfBounds =
!qEnvironmentVariableIsSet("I3SOCK") && !qEnvironmentVariableIsSet("I3SOCK") &&
std::none_of(screens.begin(), screens.end(), std::none_of(screens.begin(), screens.end(),

View file

@ -4,6 +4,8 @@
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include <QApplication>
namespace chatterino { namespace chatterino {
void GIFTimer::initialize() void GIFTimer::initialize()
@ -24,7 +26,7 @@ void GIFTimer::initialize()
QObject::connect(&this->timer, &QTimer::timeout, [this] { QObject::connect(&this->timer, &QTimer::timeout, [this] {
if (getSettings()->animationsWhenFocused && if (getSettings()->animationsWhenFocused &&
qApp->activeWindow() == nullptr) QApplication::activeWindow() == nullptr)
{ {
return; return;
} }

View file

@ -48,7 +48,7 @@ QString formatTime(int totalSeconds)
return res; return res;
} }
QString formatTime(QString totalSecondsString) QString formatTime(const QString &totalSecondsString)
{ {
bool ok = true; bool ok = true;
int totalSeconds(totalSecondsString.toInt(&ok)); int totalSeconds(totalSecondsString.toInt(&ok));

View file

@ -8,7 +8,7 @@ namespace chatterino {
// format: 1h 23m 42s // format: 1h 23m 42s
QString formatTime(int totalSeconds); QString formatTime(int totalSeconds);
QString formatTime(QString totalSecondsString); QString formatTime(const QString &totalSecondsString);
QString formatTime(std::chrono::seconds totalSeconds); QString formatTime(std::chrono::seconds totalSeconds);
} // namespace chatterino } // namespace chatterino

View file

@ -10,7 +10,7 @@ namespace chatterino {
// https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style // https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style
// Qt 5/4 - preferred, has least allocations // Qt 5/4 - preferred, has least allocations
template <typename F> template <typename F>
static void postToThread(F &&fun, QObject *obj = qApp) static void postToThread(F &&fun, QObject *obj = QCoreApplication::instance())
{ {
struct Event : public QEvent { struct Event : public QEvent {
using Fun = typename std::decay<F>::type; using Fun = typename std::decay<F>::type;

View file

@ -5,6 +5,7 @@
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "widgets/splits/Split.hpp" #include "widgets/splits/Split.hpp"
#include <QApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMessageBox> #include <QMessageBox>
@ -75,7 +76,7 @@ void FramelessEmbedWindow::showEvent(QShowEvent *)
auto handle = reinterpret_cast<HWND>(this->winId()); auto handle = reinterpret_cast<HWND>(this->winId());
if (!::SetParent(handle, parentHwnd)) if (!::SetParent(handle, parentHwnd))
{ {
qApp->exit(1); QApplication::exit(1);
} }
QJsonDocument doc; QJsonDocument doc;

View file

@ -48,6 +48,7 @@
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include <magic_enum/magic_enum_flags.hpp> #include <magic_enum/magic_enum_flags.hpp>
#include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QColor> #include <QColor>
#include <QDate> #include <QDate>

View file

@ -4,9 +4,11 @@
#include <QStringList> #include <QStringList>
using namespace chatterino;
TEST(ChatterSet, insert) TEST(ChatterSet, insert)
{ {
chatterino::ChatterSet set; ChatterSet set;
EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada")); EXPECT_FALSE(set.contains("Pajlada"));
@ -26,7 +28,7 @@ TEST(ChatterSet, insert)
TEST(ChatterSet, MaxSize) TEST(ChatterSet, MaxSize)
{ {
chatterino::ChatterSet set; ChatterSet set;
EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada")); EXPECT_FALSE(set.contains("Pajlada"));
@ -36,7 +38,7 @@ TEST(ChatterSet, MaxSize)
EXPECT_TRUE(set.contains("Pajlada")); EXPECT_TRUE(set.contains("Pajlada"));
// After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i)
{ {
set.addRecentChatter(QString("%1").arg(i)); set.addRecentChatter(QString("%1").arg(i));
} }
@ -53,7 +55,7 @@ TEST(ChatterSet, MaxSize)
TEST(ChatterSet, MaxSizeLastUsed) TEST(ChatterSet, MaxSizeLastUsed)
{ {
chatterino::ChatterSet set; ChatterSet set;
EXPECT_FALSE(set.contains("pajlada")); EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada")); EXPECT_FALSE(set.contains("Pajlada"));
@ -63,7 +65,7 @@ TEST(ChatterSet, MaxSizeLastUsed)
EXPECT_TRUE(set.contains("Pajlada")); EXPECT_TRUE(set.contains("Pajlada"));
// After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i)
{ {
set.addRecentChatter(QString("%1").arg(i)); set.addRecentChatter(QString("%1").arg(i));
} }
@ -75,7 +77,7 @@ TEST(ChatterSet, MaxSizeLastUsed)
set.addRecentChatter("pajlada"); set.addRecentChatter("pajlada");
// After another CHATTER_LIMIT-1 additional chatters, pajlada should still be there // After another CHATTER_LIMIT-1 additional chatters, pajlada should still be there
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) for (auto i = 0; i < ChatterSet::CHATTER_LIMIT - 1; ++i)
{ {
set.addRecentChatter(QString("new-%1").arg(i)); set.addRecentChatter(QString("new-%1").arg(i));
} }

View file

@ -10,7 +10,6 @@
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <optional>
using namespace chatterino; using namespace chatterino;
using namespace std::chrono_literals; using namespace std::chrono_literals;
@ -33,6 +32,8 @@ using namespace std::chrono_literals;
#ifdef RUN_PUBSUB_TESTS #ifdef RUN_PUBSUB_TESTS
namespace chatterino {
template <typename T> template <typename T>
class ReceivedMessage class ReceivedMessage
{ {
@ -451,4 +452,6 @@ TEST(TwitchPubSubClient, AutoModMessageHeld)
ASSERT_EQ(pubSub.diag.connectionsFailed, 0); ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
} }
} // namespace chatterino
#endif #endif