Fixed newly uploaded subscriber emotes not being available (#2992)

Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Paweł 2021-07-17 17:18:17 +02:00 committed by GitHub
parent 91ab8b90a0
commit 7e13564c24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 145 deletions

View file

@ -2,6 +2,7 @@
## Unversioned ## Unversioned
- Major: Newly uploaded Twitch emotes are once again present in emote picker and can be autocompleted with Tab as well. (#2992)
- Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999) - Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999)
- Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010) - Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010)
- Dev: Ubuntu packages are now available (#2936) - Dev: Ubuntu packages are now available (#2936)

View file

@ -35,7 +35,8 @@ void IvrApi::getSubage(QString userName, QString channelName,
void IvrApi::getBulkEmoteSets(QString emoteSetList, void IvrApi::getBulkEmoteSets(QString emoteSetList,
ResultCallback<QJsonArray> successCallback, ResultCallback<QJsonArray> successCallback,
IvrFailureCallback failureCallback) IvrFailureCallback failureCallback,
std::function<void()> finallyCallback)
{ {
QUrlQuery urlQuery; QUrlQuery urlQuery;
urlQuery.addQueryItem("set_id", emoteSetList); urlQuery.addQueryItem("set_id", emoteSetList);
@ -54,6 +55,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList,
<< QString(result.getData()); << QString(result.getData());
failureCallback(); failureCallback();
}) })
.finally(std::move(finallyCallback))
.execute(); .execute();
} }

View file

@ -81,10 +81,11 @@ public:
ResultCallback<IvrSubage> resultCallback, ResultCallback<IvrSubage> resultCallback,
IvrFailureCallback failureCallback); IvrFailureCallback failureCallback);
// https://api.ivr.fi/docs#tag/Twitch/paths/~1twitch~1emoteset/get // https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_emotes_sets
void getBulkEmoteSets(QString emoteSetList, void getBulkEmoteSets(QString emoteSetList,
ResultCallback<QJsonArray> successCallback, ResultCallback<QJsonArray> successCallback,
IvrFailureCallback failureCallback); IvrFailureCallback failureCallback,
std::function<void()> finallyCallback);
static void initialize(); static void initialize();

View file

@ -497,7 +497,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
if (emoteSetsChanged) if (emoteSetsChanged)
{ {
currentUser->loadUserstateEmotes(); currentUser->loadUserstateEmotes([] {});
} }
QString channelName; QString channelName;
@ -512,6 +512,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
return; return;
} }
// Checking if currentUser is a VIP or staff member
QVariant _badges = message->tag("badges"); QVariant _badges = message->tag("badges");
if (_badges.isValid()) if (_badges.isValid())
{ {
@ -524,6 +525,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
} }
} }
// Checking if currentUser is a moderator
QVariant _mod = message->tag("mod"); QVariant _mod = message->tag("mod");
if (_mod.isValid()) if (_mod.isValid())
{ {
@ -535,6 +537,24 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
} }
} }
// This will emit only once and right after user logs in to IRC - reset emote data and reload emotes
void IrcMessageHandler::handleGlobalUserStateMessage(
Communi::IrcMessage *message)
{
auto currentUser = getApp()->accounts->twitch.getCurrent();
// set received emote-sets, this time used to initially load emotes
// NOTE: this should always return true unless we reconnect
auto emoteSetsChanged = currentUser->setUserstateEmoteSets(
message->tag("emote-sets").toString().split(","));
// We should always attempt to reload emotes even on reconnections where
// emoteSetsChanged, since we want to trigger emote reloads when
// "currentUserChanged" signal is emitted
qCDebug(chatterinoTwitch) << emoteSetsChanged << message->toData();
currentUser->loadEmotes();
}
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
{ {
MessageParseArgs args; MessageParseArgs args;

View file

@ -30,6 +30,7 @@ public:
void handleClearChatMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message);
void handleClearMessageMessage(Communi::IrcMessage *message); void handleClearMessageMessage(Communi::IrcMessage *message);
void handleUserStateMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message);
void handleGlobalUserStateMessage(Communi::IrcMessage *message);
void handleWhisperMessage(Communi::IrcMessage *message); void handleWhisperMessage(Communi::IrcMessage *message);
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+ // parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+

View file

@ -21,6 +21,35 @@
#include "util/QStringHash.hpp" #include "util/QStringHash.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
namespace {
std::vector<QStringList> getEmoteSetBatches(QStringList emoteSetKeys)
{
// splitting emoteSetKeys to batches of 100, because Ivr API endpoint accepts a maximum of 100 emotesets at once
constexpr int batchSize = 100;
int batchCount = (emoteSetKeys.size() / batchSize) + 1;
std::vector<QStringList> batches;
batches.reserve(batchCount);
for (int i = 0; i < batchCount; i++)
{
QStringList batch;
int last = std::min(batchSize, emoteSetKeys.size() - batchSize * i);
for (int j = 0; j < last; j++)
{
batch.push_back(emoteSetKeys.at(j + (batchSize * i)));
}
batches.emplace_back(batch);
}
return batches;
}
} // namespace
namespace chatterino { namespace chatterino {
TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
const QString &oauthClient, const QString &userID) const QString &oauthClient, const QString &userID)
@ -116,7 +145,7 @@ void TwitchAccount::loadBlocks()
} }
}, },
[] { [] {
qDebug() << "Fetching blocks failed!"; qCWarning(chatterinoTwitch) << "Fetching blocks failed!";
}); });
} }
@ -196,76 +225,24 @@ void TwitchAccount::loadEmotes()
if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty()) if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty())
{ {
qCDebug(chatterinoTwitch) << "Missing Client ID and/or OAuth token"; qCDebug(chatterinoTwitch)
return; << "Aborted loadEmotes due to missing Client ID and/or OAuth token";
}
// Getting subscription emotes from kraken
getKraken()->getUserEmotes(
this,
[this](KrakenEmoteSets data) {
// no emotes available
if (data.emoteSets.isEmpty())
{
qCWarning(chatterinoTwitch)
<< "\"emoticon_sets\" either empty or not present in "
"Kraken::getUserEmotes response";
return; return;
} }
{ {
// Clearing emote data
auto emoteData = this->emotes_.access(); auto emoteData = this->emotes_.access();
emoteData->emoteSets.clear(); emoteData->emoteSets.clear();
emoteData->emotes.clear(); emoteData->emotes.clear();
qCDebug(chatterinoTwitch) << "Cleared emotes!";
for (auto emoteSetIt = data.emoteSets.begin();
emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
{
auto emoteSet = std::make_shared<EmoteSet>();
emoteSet->key = emoteSetIt.key();
this->loadEmoteSetData(emoteSet);
for (const auto emoteArrObj : emoteSetIt.value().toArray())
{
if (!emoteArrObj.isObject())
{
qCWarning(chatterinoTwitch)
<< QString(
"Emote value from set %1 was invalid")
.arg(emoteSet->key);
continue;
}
KrakenEmote krakenEmote(emoteArrObj.toObject());
auto id = EmoteId{krakenEmote.id};
auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
emoteSet->emotes.emplace_back(TwitchEmote{id, code});
if (!emoteSet->local)
{
auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id,
code);
emoteData->emotes.emplace(code, emote);
}
} }
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(), // TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
[](const TwitchEmote &l, const TwitchEmote &r) { // For now, this is necessary as Kraken's equivalent doesn't return all emotes
return l.name.string < r.name.string; // See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
}); this->loadUserstateEmotes([=] {
emoteData->emoteSets.emplace_back(emoteSet); // Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
} this->loadKrakenEmotes();
}
// Getting userstate emotes from Ivr
this->loadUserstateEmotes();
},
[] {
// kraken request failed
}); });
} }
@ -284,10 +261,11 @@ bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
return true; return true;
} }
void TwitchAccount::loadUserstateEmotes() void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
{ {
if (this->userstateEmoteSets_.isEmpty()) if (this->userstateEmoteSets_.isEmpty())
{ {
callback();
return; return;
} }
@ -303,7 +281,7 @@ void TwitchAccount::loadUserstateEmotes()
} }
// filter out emote sets from userstate message, which are not in fetched emote set list // filter out emote sets from userstate message, which are not in fetched emote set list
for (const auto &emoteSetKey : this->userstateEmoteSets_) for (const auto &emoteSetKey : qAsConst(this->userstateEmoteSets_))
{ {
if (!krakenEmoteSetKeys.contains(emoteSetKey)) if (!krakenEmoteSetKeys.contains(emoteSetKey))
{ {
@ -314,54 +292,50 @@ void TwitchAccount::loadUserstateEmotes()
// return if there are no new emote sets // return if there are no new emote sets
if (newEmoteSetKeys.isEmpty()) if (newEmoteSetKeys.isEmpty())
{ {
callback();
return; return;
} }
qCDebug(chatterinoTwitch) << QString("Loading %1 emotesets from IVR: %2")
.arg(newEmoteSetKeys.size())
.arg(newEmoteSetKeys.join(", "));
// splitting newEmoteSetKeys to batches of 100, because Ivr API endpoint accepts a maximum of 100 emotesets at once
constexpr int batchSize = 100;
std::vector<QStringList> batches;
int batchCount = (newEmoteSetKeys.size() / batchSize) + 1;
batches.reserve(batchCount);
for (int i = 0; i < batchCount; i++)
{
QStringList batch;
int last = std::min(batchSize, newEmoteSetKeys.size() - batchSize * i);
for (int j = batchSize * i; j < last; j++)
{
batch.push_back(newEmoteSetKeys.at(j));
}
batches.emplace_back(batch);
}
// requesting emotes // requesting emotes
for (const auto &batch : batches) auto batches = getEmoteSetBatches(newEmoteSetKeys);
for (int i = 0; i < batches.size(); i++)
{ {
qCDebug(chatterinoTwitch)
<< QString(
"Loading %1 emotesets from IVR; batch %2/%3 (%4 sets): %5")
.arg(newEmoteSetKeys.size())
.arg(i + 1)
.arg(batches.size())
.arg(batches.at(i).size())
.arg(batches.at(i).join(","));
getIvr()->getBulkEmoteSets( getIvr()->getBulkEmoteSets(
batch.join(","), batches.at(i).join(","),
[this](QJsonArray emoteSetArray) { [this](QJsonArray emoteSetArray) {
auto emoteData = this->emotes_.access(); auto emoteData = this->emotes_.access();
auto localEmoteData = this->localEmotes_.access(); auto localEmoteData = this->localEmotes_.access();
for (auto emoteSet : emoteSetArray) for (auto emoteSet_ : emoteSetArray)
{ {
auto newUserEmoteSet = std::make_shared<EmoteSet>(); auto emoteSet = std::make_shared<EmoteSet>();
IvrEmoteSet ivrEmoteSet(emoteSet.toObject()); IvrEmoteSet ivrEmoteSet(emoteSet_.toObject());
newUserEmoteSet->key = ivrEmoteSet.setId; QString setKey = ivrEmoteSet.setId;
emoteSet->key = setKey;
auto name = ivrEmoteSet.login; // check if the emoteset is already in emoteData
name.detach(); auto isAlreadyFetched =
name[0] = name[0].toUpper(); std::find_if(emoteData->emoteSets.begin(),
emoteData->emoteSets.end(),
[setKey](std::shared_ptr<EmoteSet> set) {
return (set->key == setKey);
});
if (isAlreadyFetched != emoteData->emoteSets.end())
{
continue;
}
newUserEmoteSet->text = name; emoteSet->channelName = ivrEmoteSet.login;
newUserEmoteSet->channelName = ivrEmoteSet.login; emoteSet->text = ivrEmoteSet.displayName;
for (const auto &emoteObj : ivrEmoteSet.emotes) for (const auto &emoteObj : ivrEmoteSet.emotes)
{ {
@ -371,8 +345,7 @@ void TwitchAccount::loadUserstateEmotes()
auto code = EmoteName{ auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)}; TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)};
newUserEmoteSet->emotes.push_back( emoteSet->emotes.push_back(TwitchEmote{id, code});
TwitchEmote{id, code});
auto emote = auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id, code); getApp()->emotes->twitch.getOrCreateEmote(id, code);
@ -380,7 +353,7 @@ void TwitchAccount::loadUserstateEmotes()
// Follower emotes can be only used in their origin channel // Follower emotes can be only used in their origin channel
if (ivrEmote.emoteType == "FOLLOWER") if (ivrEmote.emoteType == "FOLLOWER")
{ {
newUserEmoteSet->local = true; emoteSet->local = true;
// EmoteMap for target channel wasn't initialized yet, doing it now // EmoteMap for target channel wasn't initialized yet, doing it now
if (localEmoteData->find(ivrEmoteSet.channelId) == if (localEmoteData->find(ivrEmoteSet.channelId) ==
@ -398,16 +371,25 @@ void TwitchAccount::loadUserstateEmotes()
emoteData->emotes.emplace(code, emote); emoteData->emotes.emplace(code, emote);
} }
} }
std::sort(newUserEmoteSet->emotes.begin(), std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
newUserEmoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) { [](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string; return l.name.string < r.name.string;
}); });
emoteData->emoteSets.emplace_back(newUserEmoteSet); emoteData->emoteSets.emplace_back(emoteSet);
} }
}, },
[] { [] {
// fetching emotes failed, ivr API might be down // fetching emotes failed, ivr API might be down
},
[=] {
// XXX(zneix): We check if this is the last iteration and if so, call the callback
if (i + 1 == batches.size())
{
qCDebug(chatterinoTwitch)
<< "Finished loading emotes from IVR, attempting to "
"load Kraken emotes now";
callback();
}
}); });
}; };
} }
@ -517,6 +499,79 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel)
}); });
} }
void TwitchAccount::loadKrakenEmotes()
{
getKraken()->getUserEmotes(
this,
[this](KrakenEmoteSets data) {
// no emotes available
if (data.emoteSets.isEmpty())
{
qCWarning(chatterinoTwitch)
<< "\"emoticon_sets\" either empty or not present in "
"Kraken::getUserEmotes response";
return;
}
auto emoteData = this->emotes_.access();
for (auto emoteSetIt = data.emoteSets.begin();
emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
{
auto emoteSet = std::make_shared<EmoteSet>();
QString setKey = emoteSetIt.key();
emoteSet->key = setKey;
this->loadEmoteSetData(emoteSet);
// check if the emoteset is already in emoteData
auto isAlreadyFetched = std::find_if(
emoteData->emoteSets.begin(), emoteData->emoteSets.end(),
[setKey](std::shared_ptr<EmoteSet> set) {
return (set->key == setKey);
});
if (isAlreadyFetched != emoteData->emoteSets.end())
{
continue;
}
for (const auto emoteArrObj : emoteSetIt->toArray())
{
if (!emoteArrObj.isObject())
{
qCWarning(chatterinoTwitch)
<< QString("Emote value from set %1 was invalid")
.arg(emoteSet->key);
continue;
}
KrakenEmote krakenEmote(emoteArrObj.toObject());
auto id = EmoteId{krakenEmote.id};
auto code = EmoteName{
TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
emoteSet->emotes.emplace_back(TwitchEmote{id, code});
if (!emoteSet->local)
{
auto emote =
getApp()->emotes->twitch.getOrCreateEmote(id, code);
emoteData->emotes.emplace(code, emote);
}
}
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string;
});
emoteData->emoteSets.emplace_back(emoteSet);
}
},
[] {
// kraken request failed
});
}
void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet) void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
{ {
if (!emoteSet) if (!emoteSet)
@ -546,7 +601,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
if (emoteSetData.ownerId.isEmpty() || if (emoteSetData.ownerId.isEmpty() ||
emoteSetData.setId != emoteSet->key) emoteSetData.setId != emoteSet->key)
{ {
qCWarning(chatterinoTwitch) qCDebug(chatterinoTwitch)
<< QString("Failed to fetch emoteSetData for %1, assuming " << QString("Failed to fetch emoteSetData for %1, assuming "
"Twitch is the owner") "Twitch is the owner")
.arg(emoteSet->key); .arg(emoteSet->key);

View file

@ -115,7 +115,7 @@ public:
void loadEmotes(); void loadEmotes();
// loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key // loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key
// this function makes sure not to load emote sets that have already been loaded // this function makes sure not to load emote sets that have already been loaded
void loadUserstateEmotes(); void loadUserstateEmotes(std::function<void()> callback);
// setUserStateEmoteSets sets the emote sets that were parsed from the USERSTATE emote-sets key // setUserStateEmoteSets sets the emote sets that were parsed from the USERSTATE emote-sets key
// Returns true if the newly inserted emote sets differ from the ones previously saved // Returns true if the newly inserted emote sets differ from the ones previously saved
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets); [[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
@ -128,6 +128,7 @@ public:
void autoModDeny(const QString msgID, ChannelPtr channel); void autoModDeny(const QString msgID, ChannelPtr channel);
private: private:
void loadKrakenEmotes();
void loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet); void loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet);
QString oauthClient_; QString oauthClient_;

View file

@ -60,6 +60,19 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName(); qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName();
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands
// twitch.tv/membership enables the JOIN/PART/NAMES commands. See https://dev.twitch.tv/docs/irc/membership
// This is enabled so we receive USERSTATE messages when joining channels / typing messages, along with the other command capabilities
QStringList caps{"twitch.tv/tags", "twitch.tv/commands"};
if (type != ConnectionType::Write)
{
caps.push_back("twitch.tv/membership");
}
connection->network()->setSkipCapabilityValidation(true);
connection->network()->setRequestedCapabilities(caps);
QString username = account->getUserName(); QString username = account->getUserName();
QString oauthToken = account->getOAuthToken(); QString oauthToken = account->getOAuthToken();
@ -169,6 +182,10 @@ void TwitchIrcServer::readConnectionMessageReceived(
"Twitch Servers requested us to reconnect, reconnecting"); "Twitch Servers requested us to reconnect, reconnecting");
this->connect(); this->connect();
} }
else if (command == "GLOBALUSERSTATE")
{
handler.handleGlobalUserStateMessage(message);
}
} }
void TwitchIrcServer::writeConnectionMessageReceived( void TwitchIrcServer::writeConnectionMessageReceived(
@ -219,28 +236,6 @@ void TwitchIrcServer::writeConnectionMessageReceived(
} }
} }
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
{
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
// This is enabled here so we receive USERSTATE messages when joining channels
connection->sendRaw(
"CAP REQ :twitch.tv/tags twitch.tv/membership twitch.tv/commands");
AbstractIrcServer::onReadConnected(connection);
}
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
{
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
// This is enabled here so we receive USERSTATE messages when typing messages, along with the other command capabilities
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
AbstractIrcServer::onWriteConnected(connection);
}
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel( std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
const QString &channelName) const QString &channelName)
{ {

View file

@ -56,9 +56,6 @@ protected:
virtual void writeConnectionMessageReceived( virtual void writeConnectionMessageReceived(
Communi::IrcMessage *message) override; Communi::IrcMessage *message) override;
virtual void onReadConnected(IrcConnection *connection) override;
virtual void onWriteConnected(IrcConnection *connection) override;
virtual std::shared_ptr<Channel> getCustomChannel( virtual std::shared_ptr<Channel> getCustomChannel(
const QString &channelname) override; const QString &channelname) override;

View file

@ -11,10 +11,6 @@ Emotes::Emotes()
void Emotes::initialize(Settings &settings, Paths &paths) void Emotes::initialize(Settings &settings, Paths &paths)
{ {
getApp()->accounts->twitch.currentUserChanged.connect([] {
getApp()->accounts->twitch.getCurrent()->loadEmotes();
});
this->emojis.load(); this->emojis.load();
this->gifTimer.initialize(); this->gifTimer.initialize();