mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
refactor: load Twitch emotes from Helix (#5239)
This commit is contained in:
parent
03b0e4881f
commit
820aa12af6
40 changed files with 1251 additions and 528 deletions
|
@ -42,7 +42,7 @@ CheckOptions:
|
|||
- key: readability-identifier-naming.FunctionCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
||||
value: ^TEST$
|
||||
value: ^(TEST|MOCK_METHOD)$
|
||||
|
||||
- key: readability-identifier-naming.MemberCase
|
||||
value: camelBack
|
||||
|
|
2
.github/workflows/clang-tidy.yml
vendored
2
.github/workflows/clang-tidy.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
|||
build_dir: build-clang-tidy
|
||||
config_file: ".clang-tidy"
|
||||
split_workflow: true
|
||||
exclude: "lib/*,tools/crash-handler/*"
|
||||
exclude: "lib/*,tools/crash-handler/*,mocks/*"
|
||||
cmake_command: >-
|
||||
./.CI/setup-clang-tidy.sh
|
||||
apt_packages: >-
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504)
|
||||
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
|
||||
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
|
||||
- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239)
|
||||
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
|
||||
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
|
||||
- Bugfix: Fixed some tooltips not being readable. (#5578)
|
||||
|
|
|
@ -257,6 +257,13 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
ITwitchUsers *getTwitchUsers() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getTwitchUsers was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QTemporaryDir settingsDir;
|
||||
Paths paths_;
|
||||
Args args_;
|
||||
|
|
|
@ -410,6 +410,23 @@ public:
|
|||
(FailureCallback<HelixSendMessageError, QString> failureCallback)),
|
||||
(override));
|
||||
|
||||
// get user emotes
|
||||
MOCK_METHOD(
|
||||
void, getUserEmotes,
|
||||
(QString userID, QString broadcasterID,
|
||||
(ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||
successCallback),
|
||||
FailureCallback<QString> failureCallback, CancellationToken &&token),
|
||||
(override));
|
||||
|
||||
// get followed channel
|
||||
MOCK_METHOD(
|
||||
void, getFollowedChannel,
|
||||
(QString userID, QString broadcasterID,
|
||||
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||
FailureCallback<QString> failureCallback),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
|
||||
(override));
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
#include "providers/twitch/PubSubMessages.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchUsers.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
|
@ -176,6 +177,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
|||
, logging(new Logging(_settings))
|
||||
, linkResolver(new LinkResolver)
|
||||
, streamerMode(new StreamerMode)
|
||||
, twitchUsers(new TwitchUsers)
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(new PluginController(paths))
|
||||
#endif
|
||||
|
@ -515,6 +517,14 @@ IStreamerMode *Application::getStreamerMode()
|
|||
return this->streamerMode.get();
|
||||
}
|
||||
|
||||
ITwitchUsers *Application::getTwitchUsers()
|
||||
{
|
||||
assertInGuiThread();
|
||||
assert(this->twitchUsers);
|
||||
|
||||
return this->twitchUsers.get();
|
||||
}
|
||||
|
||||
BttvEmotes *Application::getBttvEmotes()
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
|
|
@ -53,6 +53,7 @@ class SeventvEmotes;
|
|||
class SeventvEventAPI;
|
||||
class ILinkResolver;
|
||||
class IStreamerMode;
|
||||
class ITwitchUsers;
|
||||
|
||||
class IApplication
|
||||
{
|
||||
|
@ -103,6 +104,7 @@ public:
|
|||
virtual SeventvEventAPI *getSeventvEventAPI() = 0;
|
||||
virtual ILinkResolver *getLinkResolver() = 0;
|
||||
virtual IStreamerMode *getStreamerMode() = 0;
|
||||
virtual ITwitchUsers *getTwitchUsers() = 0;
|
||||
};
|
||||
|
||||
class Application : public IApplication
|
||||
|
@ -166,6 +168,7 @@ private:
|
|||
const std::unique_ptr<Logging> logging;
|
||||
std::unique_ptr<ILinkResolver> linkResolver;
|
||||
std::unique_ptr<IStreamerMode> streamerMode;
|
||||
std::unique_ptr<ITwitchUsers> twitchUsers;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
std::unique_ptr<PluginController> plugins;
|
||||
#endif
|
||||
|
@ -215,6 +218,7 @@ public:
|
|||
|
||||
ILinkResolver *getLinkResolver() override;
|
||||
IStreamerMode *getStreamerMode() override;
|
||||
ITwitchUsers *getTwitchUsers() override;
|
||||
|
||||
private:
|
||||
void initBttvLiveUpdates();
|
||||
|
|
|
@ -404,6 +404,8 @@ set(SOURCE_FILES
|
|||
providers/twitch/TwitchIrcServer.hpp
|
||||
providers/twitch/TwitchUser.cpp
|
||||
providers/twitch/TwitchUser.hpp
|
||||
providers/twitch/TwitchUsers.cpp
|
||||
providers/twitch/TwitchUsers.hpp
|
||||
|
||||
providers/twitch/pubsubmessages/AutoMod.cpp
|
||||
providers/twitch/pubsubmessages/AutoMod.hpp
|
||||
|
@ -471,6 +473,7 @@ set(SOURCE_FILES
|
|||
util/DebugCount.hpp
|
||||
util/DisplayBadge.cpp
|
||||
util/DisplayBadge.hpp
|
||||
util/Expected.hpp
|
||||
util/FormatTime.cpp
|
||||
util/FormatTime.hpp
|
||||
util/FunctionEventFilter.cpp
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# include <IrcCommand>
|
||||
# include <IrcConnection>
|
||||
# include <IrcMessage>
|
||||
# include <nonstd/expected.hpp>
|
||||
# include <pajlada/serialize.hpp>
|
||||
# include <pajlada/settings/setting.hpp>
|
||||
# include <pajlada/settings/settinglistener.hpp>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/container_hash/hash_fwd.hpp>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QStringAlias(name) \
|
||||
namespace chatterino { \
|
||||
struct name { \
|
||||
|
@ -27,12 +29,22 @@
|
|||
return qHash(s.string); \
|
||||
} \
|
||||
}; \
|
||||
} /* namespace std */
|
||||
} /* namespace std */ \
|
||||
namespace boost { \
|
||||
template <> \
|
||||
struct hash<chatterino::name> { \
|
||||
std::size_t operator()(chatterino::name const &s) const \
|
||||
{ \
|
||||
return qHash(s.string); \
|
||||
} \
|
||||
}; \
|
||||
} /* namespace boost */
|
||||
|
||||
QStringAlias(UserName);
|
||||
QStringAlias(UserId);
|
||||
QStringAlias(Url);
|
||||
QStringAlias(Tooltip);
|
||||
QStringAlias(EmoteId);
|
||||
QStringAlias(EmoteSetId);
|
||||
QStringAlias(EmoteName);
|
||||
QStringAlias(EmoteAuthor);
|
||||
|
|
|
@ -113,8 +113,8 @@ bool appendWhisperMessageWordsLocally(const QStringList &words)
|
|||
for (int i = 2; i < words.length(); i++)
|
||||
{
|
||||
{ // Twitch emote
|
||||
auto it = accemotes.emotes.find({words[i]});
|
||||
if (it != accemotes.emotes.end())
|
||||
auto it = accemotes->find({words[i]});
|
||||
if (it != accemotes->end())
|
||||
{
|
||||
b.emplace<EmoteElement>(it->second,
|
||||
MessageElementFlag::TwitchEmote);
|
||||
|
|
|
@ -95,26 +95,16 @@ void EmoteSource::initializeFromChannel(const Channel *channel)
|
|||
// returns true also for special Twitch channels (/live, /mentions, /whispers, etc.)
|
||||
if (channel->isTwitchChannel())
|
||||
{
|
||||
if (auto user = app->getAccounts()->twitch.getCurrent())
|
||||
{
|
||||
// Twitch Emotes available globally
|
||||
auto emoteData = user->accessEmotes();
|
||||
addEmotes(emotes, emoteData->emotes, "Twitch Emote");
|
||||
|
||||
// Twitch Emotes available locally
|
||||
auto localEmoteData = user->accessLocalEmotes();
|
||||
if ((tc != nullptr) &&
|
||||
localEmoteData->find(tc->roomId()) != localEmoteData->end())
|
||||
{
|
||||
if (const auto *localEmotes = &localEmoteData->at(tc->roomId()))
|
||||
{
|
||||
addEmotes(emotes, *localEmotes, "Local Twitch Emotes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tc)
|
||||
{
|
||||
if (auto twitch = tc->localTwitchEmotes())
|
||||
{
|
||||
addEmotes(emotes, *twitch, "Local Twitch Emotes");
|
||||
}
|
||||
|
||||
auto user = getApp()->getAccounts()->twitch.getCurrent();
|
||||
addEmotes(emotes, **user->accessEmotes(), "Twitch Emote");
|
||||
|
||||
// TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define.
|
||||
if (auto bttv = tc->bttvEmotes())
|
||||
{
|
||||
|
|
|
@ -99,7 +99,7 @@ bool IgnorePhrase::containsEmote() const
|
|||
for (const auto &acc : accvec)
|
||||
{
|
||||
const auto &accemotes = *acc->accessEmotes();
|
||||
for (const auto &emote : accemotes.emotes)
|
||||
for (const auto &emote : *accemotes)
|
||||
{
|
||||
if (this->replace_.contains(emote.first.string,
|
||||
Qt::CaseSensitive))
|
||||
|
|
|
@ -31,28 +31,6 @@ void IvrApi::getSubage(QString userName, QString channelName,
|
|||
.execute();
|
||||
}
|
||||
|
||||
void IvrApi::getBulkEmoteSets(QString emoteSetList,
|
||||
ResultCallback<QJsonArray> successCallback,
|
||||
IvrFailureCallback failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("set_id", emoteSetList);
|
||||
|
||||
this->makeRequest("twitch/emotes/sets", urlQuery)
|
||||
.onSuccess([successCallback, failureCallback](auto result) {
|
||||
auto root = result.parseJsonArray();
|
||||
|
||||
successCallback(root);
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
qCWarning(chatterinoIvr)
|
||||
<< "Failed IVR API Call!" << result.formatError()
|
||||
<< QString(result.getData());
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
NetworkRequest IvrApi::makeRequest(QString url, QUrlQuery urlQuery)
|
||||
{
|
||||
assert(!url.startsWith("/"));
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
@ -32,45 +31,6 @@ struct IvrSubage {
|
|||
}
|
||||
};
|
||||
|
||||
struct IvrEmoteSet {
|
||||
const QString setId;
|
||||
const QString displayName;
|
||||
const QString login;
|
||||
const QString channelId;
|
||||
const QString tier;
|
||||
const QJsonArray emotes;
|
||||
|
||||
IvrEmoteSet(const QJsonObject &root)
|
||||
: setId(root.value("setID").toString())
|
||||
, displayName(root.value("channelName").toString())
|
||||
, login(root.value("channelLogin").toString())
|
||||
, channelId(root.value("channelID").toString())
|
||||
, tier(root.value("tier").toString())
|
||||
, emotes(root.value("emoteList").toArray())
|
||||
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct IvrEmote {
|
||||
const QString code;
|
||||
const QString id;
|
||||
const QString setId;
|
||||
const QString url;
|
||||
const QString emoteType;
|
||||
const QString imageType;
|
||||
|
||||
explicit IvrEmote(const QJsonObject &root)
|
||||
: code(root.value("code").toString())
|
||||
, id(root.value("id").toString())
|
||||
, setId(root.value("setID").toString())
|
||||
, url(TWITCH_EMOTE_TEMPLATE.arg(this->id, u"3.0"))
|
||||
, emoteType(root.value("type").toString())
|
||||
, imageType(root.value("assetType").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class IvrApi final
|
||||
{
|
||||
public:
|
||||
|
@ -79,11 +39,6 @@ public:
|
|||
ResultCallback<IvrSubage> resultCallback,
|
||||
IvrFailureCallback failureCallback);
|
||||
|
||||
// https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_emotes_sets
|
||||
void getBulkEmoteSets(QString emoteSetList,
|
||||
ResultCallback<QJsonArray> successCallback,
|
||||
IvrFailureCallback failureCallback);
|
||||
|
||||
static void initialize();
|
||||
|
||||
IvrApi() = default;
|
||||
|
|
|
@ -870,17 +870,6 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
|
|||
|
||||
void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
auto currentUser = getApp()->getAccounts()->twitch.getCurrent();
|
||||
|
||||
// set received emote-sets, used in TwitchAccount::loadUserstateEmotes
|
||||
bool emoteSetsChanged = currentUser->setUserstateEmoteSets(
|
||||
message->tag("emote-sets").toString().split(","));
|
||||
|
||||
if (emoteSetsChanged)
|
||||
{
|
||||
currentUser->loadUserstateEmotes();
|
||||
}
|
||||
|
||||
QString channelName;
|
||||
if (!trimChannelName(message->parameter(0), channelName))
|
||||
{
|
||||
|
@ -918,24 +907,6 @@ 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()->getAccounts()->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 *ircMessage)
|
||||
{
|
||||
MessageParseArgs args;
|
||||
|
|
|
@ -44,7 +44,6 @@ public:
|
|||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleGlobalUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *ircMessage);
|
||||
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||
|
|
|
@ -3,26 +3,33 @@
|
|||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Env.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/IvrApi.hpp"
|
||||
#include "providers/seventv/SeventvAPI.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchUsers.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "util/CancellationToken.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
#include <boost/unordered/unordered_flat_map.hpp>
|
||||
#include <QStringBuilder>
|
||||
#include <QThread>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace literals;
|
||||
|
||||
TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
|
||||
const QString &oauthClient, const QString &userID)
|
||||
: Account(ProviderId::Twitch)
|
||||
|
@ -31,9 +38,13 @@ TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
|
|||
, userName_(username)
|
||||
, userId_(userID)
|
||||
, isAnon_(username == ANONYMOUS_USERNAME)
|
||||
, emoteSets_(std::make_shared<TwitchEmoteSetMap>())
|
||||
, emotes_(std::make_shared<EmoteMap>())
|
||||
{
|
||||
}
|
||||
|
||||
TwitchAccount::~TwitchAccount() = default;
|
||||
|
||||
QString TwitchAccount::toString() const
|
||||
{
|
||||
return this->getUserName();
|
||||
|
@ -175,214 +186,6 @@ const std::unordered_set<QString> &TwitchAccount::blockedUserIds() const
|
|||
return this->ignoresUserIds_;
|
||||
}
|
||||
|
||||
void TwitchAccount::loadEmotes(std::weak_ptr<Channel> weakChannel)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Loading Twitch emotes for user" << this->getUserName();
|
||||
|
||||
if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty())
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Aborted loadEmotes due to missing Client ID and/or OAuth token";
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto emoteData = this->emotes_.access();
|
||||
emoteData->emoteSets.clear();
|
||||
emoteData->emotes.clear();
|
||||
qCDebug(chatterinoTwitch) << "Cleared emotes!";
|
||||
}
|
||||
|
||||
this->loadUserstateEmotes(weakChannel);
|
||||
}
|
||||
|
||||
bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
|
||||
{
|
||||
newEmoteSets.sort();
|
||||
|
||||
if (this->userstateEmoteSets_ == newEmoteSets)
|
||||
{
|
||||
// Nothing has changed
|
||||
return false;
|
||||
}
|
||||
|
||||
this->userstateEmoteSets_ = newEmoteSets;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TwitchAccount::loadUserstateEmotes(std::weak_ptr<Channel> weakChannel)
|
||||
{
|
||||
if (this->userstateEmoteSets_.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList newEmoteSetKeys, existingEmoteSetKeys;
|
||||
|
||||
auto emoteData = this->emotes_.access();
|
||||
auto userEmoteSets = emoteData->emoteSets;
|
||||
|
||||
// get list of already fetched emote sets
|
||||
for (const auto &userEmoteSet : userEmoteSets)
|
||||
{
|
||||
existingEmoteSetKeys.push_back(userEmoteSet->key);
|
||||
}
|
||||
|
||||
// filter out emote sets from userstate message, which are not in fetched emote set list
|
||||
for (const auto &emoteSetKey : this->userstateEmoteSets_)
|
||||
{
|
||||
if (!existingEmoteSetKeys.contains(emoteSetKey))
|
||||
{
|
||||
newEmoteSetKeys.push_back(emoteSetKey);
|
||||
}
|
||||
}
|
||||
|
||||
// return if there are no new emote sets
|
||||
if (newEmoteSetKeys.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// requesting emotes
|
||||
auto batches = splitListIntoBatches(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(
|
||||
batches.at(i).join(","),
|
||||
[this, weakChannel](QJsonArray emoteSetArray) {
|
||||
auto emoteData = this->emotes_.access();
|
||||
auto localEmoteData = this->localEmotes_.access();
|
||||
|
||||
std::unordered_set<QString> subscriberChannelIDs;
|
||||
std::vector<IvrEmoteSet> ivrEmoteSets;
|
||||
ivrEmoteSets.reserve(emoteSetArray.size());
|
||||
|
||||
for (auto emoteSet : emoteSetArray)
|
||||
{
|
||||
IvrEmoteSet ivrEmoteSet(emoteSet.toObject());
|
||||
if (!ivrEmoteSet.tier.isNull())
|
||||
{
|
||||
subscriberChannelIDs.insert(ivrEmoteSet.channelId);
|
||||
}
|
||||
ivrEmoteSets.emplace_back(ivrEmoteSet);
|
||||
}
|
||||
|
||||
for (const auto &emoteSet : emoteData->emoteSets)
|
||||
{
|
||||
if (emoteSet->subscriber)
|
||||
{
|
||||
subscriberChannelIDs.insert(emoteSet->channelID);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &ivrEmoteSet : ivrEmoteSets)
|
||||
{
|
||||
auto emoteSet = std::make_shared<EmoteSet>();
|
||||
|
||||
QString setKey = ivrEmoteSet.setId;
|
||||
emoteSet->key = setKey;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
emoteSet->channelID = ivrEmoteSet.channelId;
|
||||
emoteSet->channelName = ivrEmoteSet.login;
|
||||
emoteSet->text = ivrEmoteSet.displayName;
|
||||
emoteSet->subscriber = !ivrEmoteSet.tier.isNull();
|
||||
|
||||
// NOTE: If a user does not have a subscriber emote set, but a follower emote set, this logic will be wrong
|
||||
// However, that's not a realistic problem.
|
||||
bool haveSubscriberSetForChannel =
|
||||
subscriberChannelIDs.contains(ivrEmoteSet.channelId);
|
||||
|
||||
for (const auto &emoteObj : ivrEmoteSet.emotes)
|
||||
{
|
||||
IvrEmote ivrEmote(emoteObj.toObject());
|
||||
|
||||
auto id = EmoteId{ivrEmote.id};
|
||||
auto code = EmoteName{
|
||||
TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)};
|
||||
|
||||
emoteSet->emotes.push_back(TwitchEmote{id, code});
|
||||
|
||||
auto emote = getApp()
|
||||
->getEmotes()
|
||||
->getTwitchEmotes()
|
||||
->getOrCreateEmote(id, code);
|
||||
|
||||
// Follower emotes can be only used in their origin channel
|
||||
// unless the user is subscribed, then they can be used anywhere.
|
||||
if (ivrEmote.emoteType == "FOLLOWER" &&
|
||||
!haveSubscriberSetForChannel)
|
||||
{
|
||||
emoteSet->local = true;
|
||||
|
||||
// EmoteMap for target channel wasn't initialized yet, doing it now
|
||||
if (localEmoteData->find(ivrEmoteSet.channelId) ==
|
||||
localEmoteData->end())
|
||||
{
|
||||
localEmoteData->emplace(ivrEmoteSet.channelId,
|
||||
EmoteMap());
|
||||
}
|
||||
|
||||
localEmoteData->at(ivrEmoteSet.channelId)
|
||||
.emplace(code, emote);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
if (auto channel = weakChannel.lock(); channel != nullptr)
|
||||
{
|
||||
channel->addSystemMessage(
|
||||
"Twitch subscriber emotes reloaded.");
|
||||
}
|
||||
},
|
||||
[] {
|
||||
// fetching emotes failed, ivr API might be down
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
SharedAccessGuard<const TwitchAccount::TwitchAccountEmoteData>
|
||||
TwitchAccount::accessEmotes() const
|
||||
{
|
||||
return this->emotes_.accessConst();
|
||||
}
|
||||
|
||||
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
|
||||
TwitchAccount::accessLocalEmotes() const
|
||||
{
|
||||
return this->localEmotes_.accessConst();
|
||||
}
|
||||
|
||||
// AutoModActions
|
||||
void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel)
|
||||
{
|
||||
|
@ -515,4 +318,140 @@ void TwitchAccount::loadSeventvUserID()
|
|||
});
|
||||
}
|
||||
|
||||
bool TwitchAccount::hasEmoteSet(const EmoteSetId &id) const
|
||||
{
|
||||
auto emotes = this->emoteSets_.accessConst();
|
||||
return emotes->get()->contains(id);
|
||||
}
|
||||
|
||||
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
||||
TwitchAccount::accessEmoteSets() const
|
||||
{
|
||||
return this->emoteSets_.accessConst();
|
||||
}
|
||||
|
||||
SharedAccessGuard<std::shared_ptr<const EmoteMap>> TwitchAccount::accessEmotes()
|
||||
const
|
||||
{
|
||||
return this->emotes_.accessConst();
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> TwitchAccount::twitchEmote(const EmoteName &name) const
|
||||
{
|
||||
auto emotes = this->emotes_.accessConst();
|
||||
auto it = (*emotes)->find(name);
|
||||
if (it != (*emotes)->end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void TwitchAccount::reloadEmotes(void *caller)
|
||||
{
|
||||
if (this->isAnon() || getApp()->isTest())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationToken token(false);
|
||||
this->emoteToken_ = token;
|
||||
|
||||
auto sets = std::make_shared<TwitchEmoteSetMap>();
|
||||
auto emoteMap = std::make_shared<EmoteMap>();
|
||||
auto nCalls = std::make_shared<size_t>();
|
||||
|
||||
auto *twitchEmotes = getApp()->getEmotes()->getTwitchEmotes();
|
||||
auto *twitchUsers = getApp()->getTwitchUsers();
|
||||
|
||||
auto addEmote = [sets, emoteMap, twitchEmotes,
|
||||
twitchUsers](const HelixChannelEmote &emote) {
|
||||
EmoteId id{emote.id};
|
||||
EmoteName name{emote.name};
|
||||
auto meta = getTwitchEmoteSetMeta(emote);
|
||||
|
||||
auto emotePtr = twitchEmotes->getOrCreateEmote(id, name);
|
||||
if (!emoteMap->try_emplace(name, emotePtr).second)
|
||||
{
|
||||
// if the emote already exists, we don't want to add it to a set as
|
||||
// those are assumed to be disjoint
|
||||
return;
|
||||
}
|
||||
|
||||
auto set = sets->find(EmoteSetId{meta.setID});
|
||||
if (set == sets->end())
|
||||
{
|
||||
auto owner = [&]() {
|
||||
if (meta.isSubLike)
|
||||
{
|
||||
return twitchUsers->resolveID({emote.ownerID});
|
||||
}
|
||||
|
||||
return std::make_shared<TwitchUser>(TwitchUser{
|
||||
.id = u"[x-c2-global-owner]"_s,
|
||||
.name = {},
|
||||
.displayName = {},
|
||||
});
|
||||
}();
|
||||
set = sets->emplace(EmoteSetId{meta.setID},
|
||||
TwitchEmoteSet{
|
||||
.owner = owner,
|
||||
.emotes = {},
|
||||
.isBits = meta.isBits,
|
||||
.isSubLike = meta.isSubLike,
|
||||
})
|
||||
.first;
|
||||
}
|
||||
set->second.emotes.emplace_back(std::move(emotePtr));
|
||||
};
|
||||
|
||||
auto userID = this->getUserId();
|
||||
qDebug(chatterinoTwitch).nospace()
|
||||
<< "Loading Twitch emotes - userID: " << userID
|
||||
<< ", broadcasterID: none, manualRefresh: " << (caller != nullptr);
|
||||
|
||||
getHelix()->getUserEmotes(
|
||||
this->getUserId(), {},
|
||||
[this, caller, emoteMap, sets, addEmote, nCalls](
|
||||
const auto &emotes, const auto &state) mutable {
|
||||
assert(emoteMap && sets);
|
||||
(*nCalls)++;
|
||||
qDebug(chatterinoTwitch).nospace()
|
||||
<< "Got " << emotes.size() << " more emote(s)";
|
||||
|
||||
emoteMap->reserve(emoteMap->size() + emotes.size());
|
||||
for (const auto &emote : emotes)
|
||||
{
|
||||
addEmote(emote);
|
||||
}
|
||||
|
||||
if (state.done)
|
||||
{
|
||||
qDebug(chatterinoTwitch).nospace()
|
||||
<< "Loaded " << emoteMap->size() << " Twitch emotes ("
|
||||
<< *nCalls << " requests)";
|
||||
|
||||
for (auto &[id, set] : *sets)
|
||||
{
|
||||
std::ranges::sort(
|
||||
set.emotes, [](const auto &l, const auto &r) {
|
||||
return l->name.string < r->name.string;
|
||||
});
|
||||
}
|
||||
|
||||
*this->emotes_.access() = std::move(emoteMap);
|
||||
*this->emoteSets_.access() = std::move(sets);
|
||||
getApp()->getAccounts()->twitch.emotesReloaded.invoke(caller,
|
||||
{});
|
||||
}
|
||||
},
|
||||
[caller](const auto &error) {
|
||||
qDebug(chatterinoTwitch)
|
||||
<< "Failed to load Twitch emotes:" << error;
|
||||
getApp()->getAccounts()->twitch.emotesReloaded.invoke(
|
||||
caller, makeUnexpected(error));
|
||||
},
|
||||
std::move(token));
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "controllers/accounts/Account.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "providers/twitch/TwitchUser.hpp"
|
||||
#include "util/CancellationToken.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <boost/unordered/unordered_flat_map_fwd.hpp>
|
||||
#include <pajlada/signals.hpp>
|
||||
#include <QColor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
|
@ -18,7 +20,6 @@
|
|||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -28,31 +29,13 @@ using ChannelPtr = std::shared_ptr<Channel>;
|
|||
class TwitchAccount : public Account
|
||||
{
|
||||
public:
|
||||
struct TwitchEmote {
|
||||
EmoteId id;
|
||||
EmoteName name;
|
||||
};
|
||||
|
||||
struct EmoteSet {
|
||||
QString key;
|
||||
QString channelName;
|
||||
QString channelID;
|
||||
QString text;
|
||||
bool subscriber{false};
|
||||
bool local{false};
|
||||
std::vector<TwitchEmote> emotes;
|
||||
};
|
||||
|
||||
struct TwitchAccountEmoteData {
|
||||
std::vector<std::shared_ptr<EmoteSet>> emoteSets;
|
||||
|
||||
// this EmoteMap should contain all emotes available globally
|
||||
// excluding locally available emotes, such as follower ones
|
||||
EmoteMap emotes;
|
||||
};
|
||||
|
||||
TwitchAccount(const QString &username, const QString &oauthToken_,
|
||||
const QString &oauthClient_, const QString &_userID);
|
||||
~TwitchAccount() override;
|
||||
TwitchAccount(const TwitchAccount &) = delete;
|
||||
TwitchAccount(TwitchAccount &&) = delete;
|
||||
TwitchAccount &operator=(const TwitchAccount &) = delete;
|
||||
TwitchAccount &operator=(TwitchAccount &&) = delete;
|
||||
|
||||
QString toString() const override;
|
||||
|
||||
|
@ -91,23 +74,32 @@ public:
|
|||
[[nodiscard]] const std::unordered_set<TwitchUser> &blocks() const;
|
||||
[[nodiscard]] const std::unordered_set<QString> &blockedUserIds() const;
|
||||
|
||||
void loadEmotes(std::weak_ptr<Channel> weakChannel = {});
|
||||
// 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
|
||||
void loadUserstateEmotes(std::weak_ptr<Channel> weakChannel = {});
|
||||
// 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
|
||||
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
|
||||
SharedAccessGuard<const TwitchAccountEmoteData> accessEmotes() const;
|
||||
SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
|
||||
accessLocalEmotes() const;
|
||||
|
||||
// Automod actions
|
||||
void autoModAllow(const QString msgID, ChannelPtr channel);
|
||||
void autoModDeny(const QString msgID, ChannelPtr channel);
|
||||
|
||||
void loadSeventvUserID();
|
||||
|
||||
/// Returns true if the account has access to the given emote set
|
||||
bool hasEmoteSet(const EmoteSetId &id) const;
|
||||
|
||||
/// Return a map of emote sets the account has access to
|
||||
///
|
||||
/// Key being the emote set ID, and contents being information about the emote set
|
||||
/// and the emotes contained in the emote set
|
||||
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
||||
accessEmoteSets() const;
|
||||
|
||||
/// Return a map of emotes the account has access to
|
||||
SharedAccessGuard<std::shared_ptr<const EmoteMap>> accessEmotes() const;
|
||||
|
||||
/// Return the emote by emote name if the account has access to the emote
|
||||
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
||||
|
||||
/// Once emotes are reloaded, TwitchAccountManager::emotesReloaded is
|
||||
/// invoked with @a caller and an optional error.
|
||||
void reloadEmotes(void *caller = nullptr);
|
||||
|
||||
private:
|
||||
QString oauthClient_;
|
||||
QString oauthToken_;
|
||||
|
@ -122,9 +114,9 @@ private:
|
|||
std::unordered_set<TwitchUser> ignores_;
|
||||
std::unordered_set<QString> ignoresUserIds_;
|
||||
|
||||
// std::map<UserId, TwitchAccountEmoteData> emotes;
|
||||
UniqueAccess<TwitchAccountEmoteData> emotes_;
|
||||
UniqueAccess<std::unordered_map<QString, EmoteMap>> localEmotes_;
|
||||
ScopedCancellationToken emoteToken_;
|
||||
UniqueAccess<std::shared_ptr<const TwitchEmoteSetMap>> emoteSets_;
|
||||
UniqueAccess<std::shared_ptr<const EmoteMap>> emotes_;
|
||||
|
||||
QString seventvUserID_;
|
||||
};
|
||||
|
|
|
@ -162,6 +162,7 @@ void TwitchAccountManager::load()
|
|||
}
|
||||
|
||||
this->currentUserChanged();
|
||||
this->currentUser_->reloadEmotes();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "util/Expected.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
|
||||
|
@ -58,6 +59,10 @@ public:
|
|||
|
||||
SignalVector<std::shared_ptr<TwitchAccount>> accounts;
|
||||
|
||||
/// The signal is invoked with (caller, error) where caller is the argument
|
||||
/// passed to reloadEmotes() and error.
|
||||
pajlada::Signals::Signal<void *, ExpectedStr<void>> emotesReloaded;
|
||||
|
||||
private:
|
||||
enum class AddUserResponse {
|
||||
UserAlreadyExists,
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "common/Env.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
|
@ -32,6 +34,7 @@
|
|||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchUsers.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/StreamerMode.hpp"
|
||||
|
@ -47,11 +50,15 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QStringBuilder>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <rapidjson/document.h>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace literals;
|
||||
|
||||
namespace {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 1, 0)
|
||||
const QString MAGIC_MESSAGE_SUFFIX = QString((const char *)u8" \U000E0000");
|
||||
|
@ -84,6 +91,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
|
||||
, channelUrl_("https://twitch.tv/" + name)
|
||||
, popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name))
|
||||
, localTwitchEmotes_(std::make_shared<EmoteMap>())
|
||||
, bttvEmotes_(std::make_shared<EmoteMap>())
|
||||
, ffzEmotes_(std::make_shared<EmoteMap>())
|
||||
, seventvEmotes_(std::make_shared<EmoteMap>())
|
||||
|
@ -94,6 +102,7 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
getApp()->getAccounts()->twitch.currentUserChanged.connect([this] {
|
||||
this->setMod(false);
|
||||
this->refreshPubSub();
|
||||
this->refreshTwitchChannelEmotes(false);
|
||||
}));
|
||||
|
||||
this->refreshPubSub();
|
||||
|
@ -133,6 +142,36 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
});
|
||||
this->threadClearTimer_.start(5 * 60 * 1000);
|
||||
|
||||
this->signalHolder_.managedConnect(
|
||||
getApp()->getAccounts()->twitch.emotesReloaded,
|
||||
[this](auto *caller, const auto &result) {
|
||||
if (result)
|
||||
{
|
||||
// emotes were reloaded - clear follower emotes if the user is
|
||||
// now subscribed to the streamer
|
||||
if (!this->localTwitchEmotes_.get()->empty() &&
|
||||
getApp()->getAccounts()->twitch.getCurrent()->hasEmoteSet(
|
||||
EmoteSetId{this->localTwitchEmoteSetID_.get()}))
|
||||
{
|
||||
this->localTwitchEmotes_.set(std::make_shared<EmoteMap>());
|
||||
}
|
||||
|
||||
if (caller == this)
|
||||
{
|
||||
this->addSystemMessage(
|
||||
"Twitch subscriber emotes reloaded.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (caller == this || caller == nullptr)
|
||||
{
|
||||
this->addSystemMessage(
|
||||
u"Failed to load Twitch subscriber emotes: " %
|
||||
result.error());
|
||||
}
|
||||
});
|
||||
|
||||
// debugging
|
||||
#if 0
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
|
@ -194,6 +233,82 @@ void TwitchChannel::setLocalizedName(const QString &name)
|
|||
this->nameOptions.localizedName = name;
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshTwitchChannelEmotes(bool manualRefresh)
|
||||
{
|
||||
if (getApp()->isTest())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Twitch's 'Get User Emotes' doesn't assigns a different set-ID to follower
|
||||
// emotes compared to subscriber emotes.
|
||||
QString setID = TWITCH_SUB_EMOTE_SET_PREFIX % this->roomId();
|
||||
this->localTwitchEmoteSetID_.set(setID);
|
||||
if (getApp()->getAccounts()->twitch.getCurrent()->hasEmoteSet(
|
||||
EmoteSetId{setID}))
|
||||
{
|
||||
this->localTwitchEmotes_.set(std::make_shared<EmoteMap>());
|
||||
return;
|
||||
}
|
||||
|
||||
auto makeEmotes = [](const auto &emotes) {
|
||||
EmoteMap map;
|
||||
for (const auto &emote : emotes)
|
||||
{
|
||||
if (emote.type != u"follower")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
map.emplace(
|
||||
EmoteName{emote.name},
|
||||
getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
|
||||
EmoteId{emote.id}, EmoteName{emote.name}));
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
getHelix()->getFollowedChannel(
|
||||
getApp()->getAccounts()->twitch.getCurrent()->getUserId(),
|
||||
this->roomId(),
|
||||
[weak{this->weak_from_this()}, makeEmotes](const auto &chan) {
|
||||
auto self = std::dynamic_pointer_cast<TwitchChannel>(weak.lock());
|
||||
if (!self || !chan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
getHelix()->getChannelEmotes(
|
||||
self->roomId(),
|
||||
[weak, makeEmotes](const auto &emotes) {
|
||||
auto self =
|
||||
std::dynamic_pointer_cast<TwitchChannel>(weak.lock());
|
||||
if (!self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self->localTwitchEmotes_.set(
|
||||
std::make_shared<EmoteMap>(makeEmotes(emotes)));
|
||||
},
|
||||
[weak] {
|
||||
auto self = weak.lock();
|
||||
if (!self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
self->addSystemMessage("Failed to load follower emotes.");
|
||||
});
|
||||
},
|
||||
[](const auto &error) {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Failed to get following status:" << error;
|
||||
});
|
||||
|
||||
if (manualRefresh)
|
||||
{
|
||||
getApp()->getAccounts()->twitch.getCurrent()->reloadEmotes(this);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
|
||||
{
|
||||
if (!Settings::instance().enableBTTVChannelEmotes)
|
||||
|
@ -536,6 +651,7 @@ void TwitchChannel::roomIdChanged()
|
|||
this->refreshPubSub();
|
||||
this->refreshBadges();
|
||||
this->refreshCheerEmotes();
|
||||
this->refreshTwitchChannelEmotes(false);
|
||||
this->refreshFFZChannelEmotes(false);
|
||||
this->refreshBTTVChannelEmotes(false);
|
||||
this->refreshSevenTVChannelEmotes(false);
|
||||
|
@ -789,6 +905,18 @@ SharedAccessGuard<const TwitchChannel::StreamStatus>
|
|||
return this->streamStatus_.accessConst();
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> TwitchChannel::twitchEmote(const EmoteName &name) const
|
||||
{
|
||||
auto emotes = this->localTwitchEmotes();
|
||||
auto it = emotes->find(name);
|
||||
|
||||
if (it == emotes->end())
|
||||
{
|
||||
return getApp()->getAccounts()->twitch.getCurrent()->twitchEmote(name);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> TwitchChannel::bttvEmote(const EmoteName &name) const
|
||||
{
|
||||
auto emotes = this->bttvEmotes_.get();
|
||||
|
@ -825,6 +953,11 @@ std::optional<EmotePtr> TwitchChannel::seventvEmote(const EmoteName &name) const
|
|||
return it->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<const EmoteMap> TwitchChannel::localTwitchEmotes() const
|
||||
{
|
||||
return this->localTwitchEmotes_.get();
|
||||
}
|
||||
|
||||
std::shared_ptr<const EmoteMap> TwitchChannel::bttvEmotes() const
|
||||
{
|
||||
return this->bttvEmotes_.get();
|
||||
|
|
|
@ -159,13 +159,17 @@ public:
|
|||
void markConnected();
|
||||
|
||||
// Emotes
|
||||
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
||||
std::optional<EmotePtr> bttvEmote(const EmoteName &name) const;
|
||||
std::optional<EmotePtr> ffzEmote(const EmoteName &name) const;
|
||||
std::optional<EmotePtr> seventvEmote(const EmoteName &name) const;
|
||||
|
||||
std::shared_ptr<const EmoteMap> localTwitchEmotes() const;
|
||||
std::shared_ptr<const EmoteMap> bttvEmotes() const;
|
||||
std::shared_ptr<const EmoteMap> ffzEmotes() const;
|
||||
std::shared_ptr<const EmoteMap> seventvEmotes() const;
|
||||
|
||||
void refreshTwitchChannelEmotes(bool manualRefresh);
|
||||
void refreshBTTVChannelEmotes(bool manualRefresh);
|
||||
void refreshFFZChannelEmotes(bool manualRefresh);
|
||||
void refreshSevenTVChannelEmotes(bool manualRefresh);
|
||||
|
@ -391,6 +395,8 @@ private:
|
|||
protected:
|
||||
void messageRemovedFromStart(const MessagePtr &msg) override;
|
||||
|
||||
Atomic<std::shared_ptr<const EmoteMap>> localTwitchEmotes_;
|
||||
Atomic<QString> localTwitchEmoteSetID_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> seventvEmotes_;
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <QStringBuilder>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
@ -399,6 +404,22 @@ qreal getEmote3xScaleFactor(const EmoteId &id)
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace literals;
|
||||
|
||||
QString TwitchEmoteSet::title() const
|
||||
{
|
||||
if (!this->owner || this->owner->name.isEmpty())
|
||||
{
|
||||
return "Twitch";
|
||||
}
|
||||
if (this->isBits)
|
||||
{
|
||||
return this->owner->name + " (Bits)";
|
||||
}
|
||||
|
||||
return this->owner->name;
|
||||
}
|
||||
|
||||
QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode)
|
||||
{
|
||||
auto cleanCode = dirtyEmoteCode;
|
||||
|
@ -453,4 +474,44 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
|
|||
return shared;
|
||||
}
|
||||
|
||||
TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote)
|
||||
{
|
||||
// follower emotes are treated as sub emotes
|
||||
// A sub emote must have an owner or an emote-set id (otherwise it's a
|
||||
// global like "BopBop")
|
||||
bool isSub =
|
||||
(emote.type == u"subscriptions" || emote.type == u"follower") &&
|
||||
!(emote.ownerID.isEmpty() && emote.setID.isEmpty());
|
||||
bool isBits = emote.type == u"bitstier";
|
||||
bool isSubLike = isSub || isBits;
|
||||
|
||||
// A lot of emotes don't have their emote-set-id set, so we create a
|
||||
// virtual emote set that groups emotes by the owner.
|
||||
// Additionally, a lot of emote sets are small, so they're grouped together as globals.
|
||||
auto actualSetID = [&]() -> QString {
|
||||
if (!isSub && !isBits)
|
||||
{
|
||||
return u"x-c2-globals"_s;
|
||||
}
|
||||
|
||||
if (!emote.setID.isEmpty())
|
||||
{
|
||||
return emote.setID;
|
||||
}
|
||||
|
||||
if (isSub)
|
||||
{
|
||||
return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID;
|
||||
}
|
||||
// isBits
|
||||
return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID;
|
||||
}();
|
||||
|
||||
return {
|
||||
.setID = actualSetID,
|
||||
.isBits = isBits,
|
||||
.isSubLike = isSubLike,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "providers/twitch/TwitchUser.hpp"
|
||||
|
||||
#include <boost/unordered/unordered_flat_map_fwd.hpp>
|
||||
#include <QColor>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
@ -36,6 +38,45 @@ struct CheerEmoteSet {
|
|||
std::vector<CheerEmote> cheerEmotes;
|
||||
};
|
||||
|
||||
struct TwitchEmoteSet {
|
||||
/// @brief The owner of this set
|
||||
///
|
||||
/// This owner might not be resolved yet
|
||||
std::shared_ptr<TwitchUser> owner;
|
||||
|
||||
std::vector<EmotePtr> emotes;
|
||||
|
||||
/// If this is a bitstier emote set
|
||||
bool isBits = false;
|
||||
|
||||
/// @brief If this emote set is a subscriber or similar emote set
|
||||
///
|
||||
/// This includes sub and bit emotes
|
||||
bool isSubLike = false;
|
||||
|
||||
/// @brief The title of this set
|
||||
///
|
||||
/// We generate this based on the emote set's flags & owner
|
||||
QString title() const;
|
||||
};
|
||||
using TwitchEmoteSetMap = boost::unordered_flat_map<EmoteSetId, TwitchEmoteSet>;
|
||||
|
||||
struct HelixChannelEmote;
|
||||
|
||||
constexpr QStringView TWITCH_SUB_EMOTE_SET_PREFIX = u"x-c2-s-";
|
||||
constexpr QStringView TWITCH_BIT_EMOTE_SET_PREFIX = u"x-c2-b-";
|
||||
|
||||
struct TwitchEmoteSetMeta {
|
||||
QString setID;
|
||||
|
||||
/// See TwitchEmoteSet::isBits
|
||||
bool isBits = false;
|
||||
/// See TwitchEmoteSet::isSubLike
|
||||
bool isSubLike = false;
|
||||
};
|
||||
|
||||
TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote);
|
||||
|
||||
class ITwitchEmotes
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -853,10 +853,6 @@ void TwitchIrcServer::readConnectionMessageReceived(
|
|||
this->markChannelsConnected();
|
||||
this->connect();
|
||||
}
|
||||
else if (command == "GLOBALUSERSTATE")
|
||||
{
|
||||
handler.handleGlobalUserStateMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#include "providers/twitch/TwitchUser.hpp"
|
||||
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -12,4 +12,12 @@ void TwitchUser::fromHelixBlock(const HelixBlock &ignore)
|
|||
this->displayName = ignore.displayName;
|
||||
}
|
||||
|
||||
void TwitchUser::update(const HelixUser &user) const
|
||||
{
|
||||
assertInGuiThread();
|
||||
assert(this->id == user.id);
|
||||
this->name = user.login;
|
||||
this->displayName = user.displayName;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
namespace chatterino {
|
||||
|
||||
struct HelixBlock;
|
||||
struct HelixUser;
|
||||
|
||||
struct TwitchUser {
|
||||
QString id;
|
||||
|
@ -26,6 +27,8 @@ struct TwitchUser {
|
|||
this->displayName = other.displayName;
|
||||
}
|
||||
|
||||
void update(const HelixUser &user) const;
|
||||
|
||||
void fromHelixBlock(const HelixBlock &ignore);
|
||||
|
||||
bool operator<(const TwitchUser &rhs) const
|
||||
|
|
147
src/providers/twitch/TwitchUsers.cpp
Normal file
147
src/providers/twitch/TwitchUsers.cpp
Normal file
|
@ -0,0 +1,147 @@
|
|||
#include "providers/twitch/TwitchUsers.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/TwitchUser.hpp"
|
||||
|
||||
#include <boost/unordered/unordered_flat_map.hpp>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
|
||||
auto withSelf(auto *ptr, auto cb)
|
||||
{
|
||||
return [weak{ptr->weak_from_this()}, cb = std::move(cb)](auto &&...args) {
|
||||
auto self = weak.lock();
|
||||
if (!self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cb(std::move(self), std::forward<decltype(args)>(args)...);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchUsersPrivate
|
||||
: public std::enable_shared_from_this<TwitchUsersPrivate>
|
||||
{
|
||||
public:
|
||||
TwitchUsersPrivate();
|
||||
|
||||
private:
|
||||
boost::unordered_flat_map<UserId, std::shared_ptr<TwitchUser>> cache;
|
||||
QStringList unresolved;
|
||||
QTimer nextBatchTimer;
|
||||
bool isResolving = false;
|
||||
|
||||
std::shared_ptr<TwitchUser> makeUnresolved(const UserId &id);
|
||||
void makeNextRequest();
|
||||
void updateUsers(const std::vector<HelixUser> &users);
|
||||
|
||||
friend TwitchUsers;
|
||||
};
|
||||
|
||||
TwitchUsers::TwitchUsers()
|
||||
: private_(new TwitchUsersPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
TwitchUsers::~TwitchUsers() = default;
|
||||
|
||||
std::shared_ptr<TwitchUser> TwitchUsers::resolveID(const UserId &id)
|
||||
{
|
||||
auto cached = this->private_->cache.find(id);
|
||||
if (cached != this->private_->cache.end())
|
||||
{
|
||||
return cached->second;
|
||||
}
|
||||
return this->private_->makeUnresolved(id);
|
||||
}
|
||||
|
||||
TwitchUsersPrivate::TwitchUsersPrivate()
|
||||
{
|
||||
this->nextBatchTimer.setSingleShot(true);
|
||||
// Wait for multiple request batches to come in before making a request
|
||||
this->nextBatchTimer.setInterval(250);
|
||||
|
||||
QObject::connect(&this->nextBatchTimer, &QTimer::timeout, [this] {
|
||||
this->makeNextRequest();
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchUser> TwitchUsersPrivate::makeUnresolved(const UserId &id)
|
||||
{
|
||||
// assumption: Cache entry is empty so neither a shared pointer was created
|
||||
// nor an entry in the unresolved list was added.
|
||||
auto ptr = this->cache
|
||||
.emplace(id, std::make_shared<TwitchUser>(TwitchUser{
|
||||
.id = id.string,
|
||||
.name = {},
|
||||
.displayName = {},
|
||||
}))
|
||||
.first->second;
|
||||
if (id.string.isEmpty())
|
||||
{
|
||||
return ptr;
|
||||
}
|
||||
|
||||
this->unresolved.append(id.string);
|
||||
if (!this->isResolving && !this->nextBatchTimer.isActive())
|
||||
{
|
||||
this->nextBatchTimer.start();
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void TwitchUsersPrivate::makeNextRequest()
|
||||
{
|
||||
if (this->unresolved.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->isResolving)
|
||||
{
|
||||
qCWarning(chatterinoTwitch) << "Tried to start request while resolving";
|
||||
return;
|
||||
}
|
||||
this->isResolving = true;
|
||||
|
||||
auto ids = this->unresolved.mid(
|
||||
0, std::min<qsizetype>(this->unresolved.size(), 100));
|
||||
this->unresolved = this->unresolved.mid(ids.length());
|
||||
getHelix()->fetchUsers(ids, {},
|
||||
withSelf(this,
|
||||
[](auto self, const auto &users) {
|
||||
self->updateUsers(users);
|
||||
self->isResolving = false;
|
||||
self->makeNextRequest();
|
||||
}),
|
||||
withSelf(this, [](auto self) {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Failed to load users";
|
||||
self->isResolving = false;
|
||||
self->makeNextRequest();
|
||||
}));
|
||||
}
|
||||
|
||||
void TwitchUsersPrivate::updateUsers(const std::vector<HelixUser> &users)
|
||||
{
|
||||
for (const auto &user : users)
|
||||
{
|
||||
auto cached = this->cache.find(UserId{user.id});
|
||||
if (cached == this->cache.end())
|
||||
{
|
||||
qCWarning(chatterinoTwitch) << "Couldn't find user" << user.login
|
||||
<< "with id" << user.id << "in cache";
|
||||
continue;
|
||||
}
|
||||
cached->second->update(user);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
55
src/providers/twitch/TwitchUsers.hpp
Normal file
55
src/providers/twitch/TwitchUsers.hpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct TwitchUser;
|
||||
class TwitchChannel;
|
||||
|
||||
class ITwitchUsers
|
||||
{
|
||||
public:
|
||||
ITwitchUsers() = default;
|
||||
virtual ~ITwitchUsers() = default;
|
||||
ITwitchUsers(const ITwitchUsers &) = delete;
|
||||
ITwitchUsers(ITwitchUsers &&) = delete;
|
||||
ITwitchUsers &operator=(const ITwitchUsers &) = delete;
|
||||
ITwitchUsers &operator=(ITwitchUsers &&) = delete;
|
||||
|
||||
/// @brief Resolve a TwitchUser by their ID
|
||||
///
|
||||
/// Users are cached. If the user wasn't resolved yet, a request will be
|
||||
/// scheduled. The returned shared pointer must only be used on the GUI
|
||||
/// thread as it will be updated from there.
|
||||
///
|
||||
/// @returns A shared reference to the TwitchUser. The `name` and
|
||||
/// `displayName` might be empty if the user wasn't resolved yet or
|
||||
/// they don't exist.
|
||||
virtual std::shared_ptr<TwitchUser> resolveID(const UserId &id) = 0;
|
||||
};
|
||||
|
||||
class TwitchUsersPrivate;
|
||||
class TwitchUsers : public ITwitchUsers
|
||||
{
|
||||
public:
|
||||
TwitchUsers();
|
||||
~TwitchUsers() override;
|
||||
TwitchUsers(const TwitchUsers &) = delete;
|
||||
TwitchUsers(TwitchUsers &&) = delete;
|
||||
TwitchUsers &operator=(const TwitchUsers &) = delete;
|
||||
TwitchUsers &operator=(TwitchUsers &&) = delete;
|
||||
|
||||
/// @see ITwitchUsers::resolveID()
|
||||
std::shared_ptr<TwitchUser> resolveID(const UserId &id) override;
|
||||
|
||||
private:
|
||||
// Using a shared_ptr to pass to network callbacks
|
||||
std::shared_ptr<TwitchUsersPrivate> private_;
|
||||
|
||||
friend TwitchUsersPrivate;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -540,7 +540,8 @@ void Helix::loadBlocks(QString userId,
|
|||
size_t receivedItems = 0;
|
||||
this->paginate(
|
||||
u"users/blocks"_s, query,
|
||||
[pageCallback, receivedItems](const QJsonObject &json) mutable {
|
||||
[pageCallback, receivedItems](const QJsonObject &json,
|
||||
const auto & /*state*/) mutable {
|
||||
const auto data = json["data"_L1].toArray();
|
||||
|
||||
if (data.isEmpty())
|
||||
|
@ -3063,6 +3064,123 @@ void Helix::sendChatMessage(
|
|||
.execute();
|
||||
}
|
||||
|
||||
void Helix::getUserEmotes(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||
pageCallback,
|
||||
FailureCallback<QString> failureCallback, CancellationToken &&token)
|
||||
{
|
||||
QUrlQuery query{{u"user_id"_s, userID}};
|
||||
if (!broadcasterID.isEmpty())
|
||||
{
|
||||
query.addQueryItem(u"broadcaster_id"_s, broadcasterID);
|
||||
}
|
||||
|
||||
this->paginate(
|
||||
u"chat/emotes/user"_s, query,
|
||||
[pageCallback](const QJsonObject &json, const auto &state) mutable {
|
||||
const auto data = json["data"_L1].toArray();
|
||||
|
||||
if (data.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<HelixChannelEmote> emotes;
|
||||
emotes.reserve(data.count());
|
||||
|
||||
for (const auto &emote : data)
|
||||
{
|
||||
emotes.emplace_back(emote.toObject());
|
||||
}
|
||||
|
||||
pageCallback(emotes, state);
|
||||
|
||||
return true;
|
||||
},
|
||||
[failureCallback](const NetworkResult &result) {
|
||||
if (!result.status())
|
||||
{
|
||||
failureCallback(result.formatError());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto obj = result.parseJson();
|
||||
auto message = obj["message"].toString();
|
||||
|
||||
switch (*result.status())
|
||||
{
|
||||
case 401: {
|
||||
if (message.startsWith("Missing scope",
|
||||
Qt::CaseInsensitive))
|
||||
{
|
||||
failureCallback("Missing required scope. Re-login with "
|
||||
"your account and try again.");
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
default: {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Helix get user emotes, unhandled error data:"
|
||||
<< result.formatError() << result.getData() << obj;
|
||||
failureCallback(message);
|
||||
}
|
||||
}
|
||||
},
|
||||
std::move(token));
|
||||
}
|
||||
|
||||
void Helix::getFollowedChannel(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||
FailureCallback<QString> failureCallback)
|
||||
{
|
||||
this->makeGet("channels/followed",
|
||||
{
|
||||
{u"user_id"_s, userID},
|
||||
{u"broadcaster_id"_s, broadcasterID},
|
||||
})
|
||||
.onSuccess([successCallback](auto result) {
|
||||
if (result.status() != 200)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Success result for getting badges was "
|
||||
<< result.formatError() << "but we expected it to be 200";
|
||||
}
|
||||
|
||||
const auto response = result.parseJson();
|
||||
const auto channel = response["data"_L1].toArray().at(0);
|
||||
if (channel.isObject())
|
||||
{
|
||||
successCallback(HelixFollowedChannel(channel.toObject()));
|
||||
}
|
||||
else
|
||||
{
|
||||
successCallback(std::nullopt);
|
||||
}
|
||||
})
|
||||
.onError([failureCallback](const auto &result) -> void {
|
||||
if (!result.status())
|
||||
{
|
||||
failureCallback(result.formatError());
|
||||
return;
|
||||
}
|
||||
|
||||
auto obj = result.parseJson();
|
||||
auto message = obj.value("message").toString();
|
||||
if (!message.isEmpty())
|
||||
{
|
||||
failureCallback(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
failureCallback(result.formatError());
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery,
|
||||
NetworkRequestType type)
|
||||
{
|
||||
|
@ -3120,8 +3238,10 @@ NetworkRequest Helix::makePatch(const QString &url, const QUrlQuery &urlQuery)
|
|||
return this->makeRequest(url, urlQuery, NetworkRequestType::Patch);
|
||||
}
|
||||
|
||||
void Helix::paginate(const QString &url, const QUrlQuery &baseQuery,
|
||||
std::function<bool(const QJsonObject &)> onPage,
|
||||
void Helix::paginate(
|
||||
const QString &url, const QUrlQuery &baseQuery,
|
||||
std::function<bool(const QJsonObject &, const HelixPaginationState &state)>
|
||||
onPage,
|
||||
std::function<void(NetworkResult)> onError,
|
||||
CancellationToken &&cancellationToken)
|
||||
{
|
||||
|
@ -3143,14 +3263,19 @@ void Helix::paginate(const QString &url, const QUrlQuery &baseQuery,
|
|||
}
|
||||
|
||||
const auto json = res.parseJson();
|
||||
if (!onPage(json))
|
||||
const auto pagination = json["pagination"_L1].toObject();
|
||||
|
||||
auto cursor = pagination["cursor"_L1].toString();
|
||||
HelixPaginationState state{.done = cursor.isEmpty()};
|
||||
|
||||
if (!onPage(json, state))
|
||||
{
|
||||
// The consumer doesn't want any more pages
|
||||
return;
|
||||
}
|
||||
|
||||
auto cursor = json["pagination"_L1]["cursor"_L1].toString();
|
||||
if (cursor.isEmpty())
|
||||
// After done is set, onPage must never be called again
|
||||
if (state.done)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -266,18 +266,18 @@ struct HelixEmoteSetData {
|
|||
};
|
||||
|
||||
struct HelixChannelEmote {
|
||||
const QString emoteId;
|
||||
const QString id;
|
||||
const QString name;
|
||||
const QString type;
|
||||
const QString setId;
|
||||
const QString url;
|
||||
const QString setID;
|
||||
const QString ownerID;
|
||||
|
||||
explicit HelixChannelEmote(QJsonObject jsonObject)
|
||||
: emoteId(jsonObject.value("id").toString())
|
||||
, name(jsonObject.value("name").toString())
|
||||
, type(jsonObject.value("emote_type").toString())
|
||||
, setId(jsonObject.value("emote_set_id").toString())
|
||||
, url(TWITCH_EMOTE_TEMPLATE.arg(this->emoteId, u"3.0"))
|
||||
explicit HelixChannelEmote(const QJsonObject &jsonObject)
|
||||
: id(jsonObject["id"].toString())
|
||||
, name(jsonObject["name"].toString())
|
||||
, type(jsonObject["emote_type"].toString())
|
||||
, setID(jsonObject["emote_set_id"].toString())
|
||||
, ownerID(jsonObject["owner_id"].toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
@ -438,6 +438,22 @@ struct HelixSentMessage {
|
|||
}
|
||||
};
|
||||
|
||||
struct HelixFollowedChannel {
|
||||
QString broadcasterID;
|
||||
QString broadcasterLogin;
|
||||
QString broadcasterName;
|
||||
QDateTime followedAt;
|
||||
|
||||
explicit HelixFollowedChannel(const QJsonObject &jsonObject)
|
||||
: broadcasterID(jsonObject["broadcaster_id"].toString())
|
||||
, broadcasterLogin(jsonObject["broadcaster_login"].toString())
|
||||
, broadcasterName(jsonObject["broadcaster_name"].toString())
|
||||
, followedAt(QDateTime::fromString(jsonObject["followed_at"].toString(),
|
||||
Qt::ISODate))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixSendMessageArgs {
|
||||
QString broadcasterID;
|
||||
QString senderID;
|
||||
|
@ -786,6 +802,10 @@ struct HelixError {
|
|||
|
||||
using HelixGetChannelBadgesError = HelixGetGlobalBadgesError;
|
||||
|
||||
struct HelixPaginationState {
|
||||
bool done;
|
||||
};
|
||||
|
||||
class IHelix
|
||||
{
|
||||
public:
|
||||
|
@ -1112,6 +1132,21 @@ public:
|
|||
ResultCallback<HelixSentMessage> successCallback,
|
||||
FailureCallback<HelixSendMessageError, QString> failureCallback) = 0;
|
||||
|
||||
/// https://dev.twitch.tv/docs/api/reference/#get-user-emotes
|
||||
virtual void getUserEmotes(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||
pageCallback,
|
||||
FailureCallback<QString> failureCallback,
|
||||
CancellationToken &&token) = 0;
|
||||
|
||||
/// https://dev.twitch.tv/docs/api/reference/#get-followed-channels
|
||||
/// (non paginated)
|
||||
virtual void getFollowedChannel(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||
FailureCallback<QString> failureCallback) = 0;
|
||||
|
||||
virtual void update(QString clientId, QString oauthToken) = 0;
|
||||
|
||||
protected:
|
||||
|
@ -1440,6 +1475,21 @@ public:
|
|||
ResultCallback<HelixSentMessage> successCallback,
|
||||
FailureCallback<HelixSendMessageError, QString> failureCallback) final;
|
||||
|
||||
/// https://dev.twitch.tv/docs/api/reference/#get-user-emotes
|
||||
void getUserEmotes(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::vector<HelixChannelEmote>, HelixPaginationState>
|
||||
pageCallback,
|
||||
FailureCallback<QString> failureCallback,
|
||||
CancellationToken &&token) final;
|
||||
|
||||
/// https://dev.twitch.tv/docs/api/reference/#get-followed-channels
|
||||
/// (non paginated)
|
||||
void getFollowedChannel(
|
||||
QString userID, QString broadcasterID,
|
||||
ResultCallback<std::optional<HelixFollowedChannel>> successCallback,
|
||||
FailureCallback<QString> failureCallback) final;
|
||||
|
||||
void update(QString clientId, QString oauthToken) final;
|
||||
|
||||
static void initialize();
|
||||
|
@ -1494,7 +1544,9 @@ private:
|
|||
/// Paginate the `url` endpoint and use `baseQuery` as the starting point for pagination.
|
||||
/// @param onPage returns true while a new page is expected. Once false is returned, pagination will stop.
|
||||
void paginate(const QString &url, const QUrlQuery &baseQuery,
|
||||
std::function<bool(const QJsonObject &)> onPage,
|
||||
std::function<bool(const QJsonObject &,
|
||||
const HelixPaginationState &state)>
|
||||
onPage,
|
||||
std::function<void(NetworkResult)> onError,
|
||||
CancellationToken &&token);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -10,24 +11,30 @@ namespace chatterino {
|
|||
class CancellationToken
|
||||
{
|
||||
public:
|
||||
CancellationToken() = default;
|
||||
CancellationToken() noexcept = default;
|
||||
explicit CancellationToken(bool isCancelled)
|
||||
: isCancelled_(new std::atomic<bool>(isCancelled))
|
||||
{
|
||||
}
|
||||
|
||||
CancellationToken(const CancellationToken &) = default;
|
||||
CancellationToken(CancellationToken &&other)
|
||||
CancellationToken(CancellationToken &&other) noexcept
|
||||
: isCancelled_(std::move(other.isCancelled_)){};
|
||||
|
||||
CancellationToken &operator=(CancellationToken &&other)
|
||||
/// @brief This destructor doesn't cancel the token
|
||||
///
|
||||
/// @see ScopedCancellationToken
|
||||
/// @see #cancel()
|
||||
~CancellationToken() noexcept = default;
|
||||
|
||||
CancellationToken &operator=(CancellationToken &&other) noexcept
|
||||
{
|
||||
this->isCancelled_ = std::move(other.isCancelled_);
|
||||
return *this;
|
||||
}
|
||||
CancellationToken &operator=(const CancellationToken &) = default;
|
||||
|
||||
void cancel()
|
||||
void cancel() noexcept
|
||||
{
|
||||
if (this->isCancelled_ != nullptr)
|
||||
{
|
||||
|
@ -35,7 +42,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
bool isCancelled() const
|
||||
bool isCancelled() const noexcept
|
||||
{
|
||||
return this->isCancelled_ == nullptr ||
|
||||
this->isCancelled_->load(std::memory_order_acquire);
|
||||
|
@ -50,30 +57,35 @@ class ScopedCancellationToken
|
|||
{
|
||||
public:
|
||||
ScopedCancellationToken() = default;
|
||||
ScopedCancellationToken(CancellationToken &&backingToken)
|
||||
: backingToken_(std::move(backingToken))
|
||||
{
|
||||
}
|
||||
ScopedCancellationToken(CancellationToken backingToken)
|
||||
explicit ScopedCancellationToken(CancellationToken backingToken)
|
||||
: backingToken_(std::move(backingToken))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedCancellationToken(const ScopedCancellationToken &) = delete;
|
||||
ScopedCancellationToken(ScopedCancellationToken &&other) noexcept
|
||||
: backingToken_(std::move(other.backingToken_)){};
|
||||
|
||||
~ScopedCancellationToken()
|
||||
{
|
||||
this->backingToken_.cancel();
|
||||
}
|
||||
|
||||
ScopedCancellationToken(const ScopedCancellationToken &) = delete;
|
||||
ScopedCancellationToken(ScopedCancellationToken &&other)
|
||||
: backingToken_(std::move(other.backingToken_)){};
|
||||
ScopedCancellationToken &operator=(ScopedCancellationToken &&other)
|
||||
ScopedCancellationToken &operator=(CancellationToken token) noexcept
|
||||
{
|
||||
this->backingToken_.cancel();
|
||||
this->backingToken_ = std::move(token);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ScopedCancellationToken &operator=(const ScopedCancellationToken &) =
|
||||
delete;
|
||||
ScopedCancellationToken &operator=(ScopedCancellationToken &&other) noexcept
|
||||
{
|
||||
this->backingToken_.cancel();
|
||||
this->backingToken_ = std::move(other.backingToken_);
|
||||
return *this;
|
||||
}
|
||||
ScopedCancellationToken &operator=(const ScopedCancellationToken &) =
|
||||
delete;
|
||||
|
||||
private:
|
||||
CancellationToken backingToken_;
|
||||
|
|
22
src/util/Expected.hpp
Normal file
22
src/util/Expected.hpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include <nonstd/expected.hpp>
|
||||
|
||||
class QString;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
template <typename T, typename E>
|
||||
using Expected = nonstd::expected_lite::expected<T, E>;
|
||||
|
||||
template <typename T>
|
||||
using ExpectedStr = Expected<T, QString>;
|
||||
|
||||
// convenience function from nonstd/expected.hpp
|
||||
template <typename E>
|
||||
constexpr nonstd::unexpected<std::decay_t<E>> makeUnexpected(E &&value)
|
||||
{
|
||||
return nonstd::unexpected<std::decay_t<E>>(std::forward<E>(value));
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
#ifdef USEWINSDK
|
||||
# include <dwmapi.h>
|
||||
# include <shellapi.h>
|
||||
# include <VersionHelpers.h>
|
||||
# include <Windows.h>
|
||||
# include <windowsx.h>
|
||||
|
@ -32,7 +33,6 @@
|
|||
# include <QHBoxLayout>
|
||||
# include <QMargins>
|
||||
# include <QOperatingSystemVersion>
|
||||
# include <QWindow>
|
||||
#endif
|
||||
|
||||
#include "widgets/helper/TitlebarButton.hpp"
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <QAbstractButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringBuilder>
|
||||
#include <QTabWidget>
|
||||
|
||||
#include <utility>
|
||||
|
@ -42,13 +43,14 @@ auto makeTitleMessage(const QString &title)
|
|||
return builder.release();
|
||||
}
|
||||
|
||||
auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
||||
auto makeEmoteMessage(std::vector<EmotePtr> emotes,
|
||||
const MessageElementFlag &emoteFlag)
|
||||
{
|
||||
MessageBuilder builder;
|
||||
builder->flags.set(MessageFlag::Centered);
|
||||
builder->flags.set(MessageFlag::DisableCompactEmotes);
|
||||
|
||||
if (map.empty())
|
||||
if (emotes.empty())
|
||||
{
|
||||
builder.emplace<TextElement>("no emotes available",
|
||||
MessageElementFlag::Text,
|
||||
|
@ -56,24 +58,41 @@ auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
|||
return builder.release();
|
||||
}
|
||||
|
||||
std::vector<std::pair<EmoteName, EmotePtr>> vec(map.begin(), map.end());
|
||||
std::sort(vec.begin(), vec.end(),
|
||||
[](const std::pair<EmoteName, EmotePtr> &l,
|
||||
const std::pair<EmoteName, EmotePtr> &r) {
|
||||
return compareEmoteStrings(l.first.string, r.first.string);
|
||||
std::sort(emotes.begin(), emotes.end(), [](const auto &l, const auto &r) {
|
||||
return compareEmoteStrings(l->name.string, r->name.string);
|
||||
});
|
||||
for (const auto &emote : vec)
|
||||
for (const auto &emote : emotes)
|
||||
{
|
||||
builder
|
||||
.emplace<EmoteElement>(
|
||||
emote.second,
|
||||
emote,
|
||||
MessageElementFlags{MessageElementFlag::AlwaysShow, emoteFlag})
|
||||
->setLink(Link(Link::InsertText, emote.first.string));
|
||||
->setLink(Link(Link::InsertText, emote->name.string));
|
||||
}
|
||||
|
||||
return builder.release();
|
||||
}
|
||||
|
||||
auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
|
||||
{
|
||||
if (map.empty())
|
||||
{
|
||||
MessageBuilder builder;
|
||||
builder.emplace<TextElement>("no emotes available",
|
||||
MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
return builder.release();
|
||||
}
|
||||
|
||||
std::vector<EmotePtr> vec;
|
||||
vec.reserve(map.size());
|
||||
for (const auto &[_name, ptr] : map)
|
||||
{
|
||||
vec.emplace_back(ptr);
|
||||
}
|
||||
return makeEmoteMessage(std::move(vec), emoteFlag);
|
||||
}
|
||||
|
||||
auto makeEmojiMessage(const std::vector<EmojiPtr> &emojiMap)
|
||||
{
|
||||
MessageBuilder builder;
|
||||
|
@ -94,77 +113,46 @@ auto makeEmojiMessage(const std::vector<EmojiPtr> &emojiMap)
|
|||
return builder.release();
|
||||
}
|
||||
|
||||
void addTwitchEmoteSets(
|
||||
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> sets,
|
||||
Channel &globalChannel, Channel &subChannel, QString currentChannelName)
|
||||
void addEmotes(Channel &channel, auto emotes, const QString &title,
|
||||
const MessageElementFlag &emoteFlag)
|
||||
{
|
||||
QMap<QString, QPair<bool, std::vector<MessagePtr>>> mapOfSets;
|
||||
channel.addMessage(makeTitleMessage(title), MessageContext::Original);
|
||||
channel.addMessage(makeEmoteMessage(emotes, emoteFlag),
|
||||
MessageContext::Original);
|
||||
}
|
||||
|
||||
for (const auto &set : sets)
|
||||
void addTwitchEmoteSets(const std::shared_ptr<const EmoteMap> &local,
|
||||
const std::shared_ptr<const TwitchEmoteSetMap> &sets,
|
||||
Channel &globalChannel, Channel &subChannel,
|
||||
const QString ¤tChannelID,
|
||||
const QString &channelName)
|
||||
{
|
||||
if (!local->empty())
|
||||
{
|
||||
// Some emotes (e.g. follower ones) are only available in their origin channel
|
||||
if (set->local && currentChannelName != set->channelName)
|
||||
addEmotes(subChannel, *local, channelName % u" (follower)",
|
||||
MessageElementFlag::TwitchEmote);
|
||||
}
|
||||
|
||||
// Put current channel emotes at the top
|
||||
for (const auto &[_id, set] : *sets)
|
||||
{
|
||||
if (set.owner->id == currentChannelID)
|
||||
{
|
||||
addEmotes(subChannel, set.emotes, set.title(),
|
||||
MessageElementFlag::TwitchEmote);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[id, set] : *sets)
|
||||
{
|
||||
if (set.owner->id == currentChannelID)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TITLE
|
||||
auto channelName = set->channelName;
|
||||
auto text = set->text.isEmpty() ? "Twitch" : set->text;
|
||||
|
||||
// EMOTES
|
||||
MessageBuilder builder;
|
||||
builder->flags.set(MessageFlag::Centered);
|
||||
builder->flags.set(MessageFlag::DisableCompactEmotes);
|
||||
|
||||
// If value of map is empty, create init pair and add title.
|
||||
if (mapOfSets.find(channelName) == mapOfSets.end())
|
||||
{
|
||||
std::vector<MessagePtr> b;
|
||||
b.push_back(makeTitleMessage(text));
|
||||
mapOfSets[channelName] = qMakePair(set->key == "0", b);
|
||||
addEmotes(set.isSubLike ? subChannel : globalChannel, set.emotes,
|
||||
set.title(), MessageElementFlag::TwitchEmote);
|
||||
}
|
||||
|
||||
for (const auto &emote : set->emotes)
|
||||
{
|
||||
builder
|
||||
.emplace<EmoteElement>(
|
||||
getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
|
||||
emote.id, emote.name),
|
||||
MessageElementFlags{MessageElementFlag::AlwaysShow,
|
||||
MessageElementFlag::TwitchEmote})
|
||||
->setLink(Link(Link::InsertText, emote.name.string));
|
||||
}
|
||||
|
||||
mapOfSets[channelName].second.push_back(builder.release());
|
||||
}
|
||||
|
||||
// Output to channel all created messages,
|
||||
// That contain title or emotes.
|
||||
// Put current channel emotes at the top
|
||||
auto currentChannelPair = mapOfSets[currentChannelName];
|
||||
for (const auto &message : currentChannelPair.second)
|
||||
{
|
||||
subChannel.addMessage(message, MessageContext::Original);
|
||||
}
|
||||
mapOfSets.remove(currentChannelName);
|
||||
|
||||
for (const auto &pair : mapOfSets)
|
||||
{
|
||||
auto &channel = pair.first ? globalChannel : subChannel;
|
||||
for (const auto &message : pair.second)
|
||||
{
|
||||
channel.addMessage(message, MessageContext::Original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addEmotes(Channel &channel, const EmoteMap &map, const QString &title,
|
||||
const MessageElementFlag &emoteFlag)
|
||||
{
|
||||
channel.addMessage(makeTitleMessage(title), MessageContext::Original);
|
||||
channel.addMessage(makeEmoteMessage(map, emoteFlag),
|
||||
MessageContext::Original);
|
||||
}
|
||||
|
||||
void loadEmojis(ChannelView &view, const std::vector<EmojiPtr> &emojiMap)
|
||||
|
@ -200,6 +188,22 @@ EmoteMap filterEmoteMap(const QString &text,
|
|||
return filteredMap;
|
||||
}
|
||||
|
||||
std::vector<EmotePtr> filterEmoteVec(const QString &text,
|
||||
const std::vector<EmotePtr> &emotes)
|
||||
{
|
||||
std::vector<EmotePtr> filtered;
|
||||
|
||||
for (const auto &emote : emotes)
|
||||
{
|
||||
if (emote->name.string.contains(text, Qt::CaseInsensitive))
|
||||
{
|
||||
filtered.emplace_back(emote);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -404,8 +408,10 @@ void EmotePopup::loadChannel(ChannelPtr channel)
|
|||
|
||||
// twitch
|
||||
addTwitchEmoteSets(
|
||||
getApp()->getAccounts()->twitch.getCurrent()->accessEmotes()->emoteSets,
|
||||
*globalChannel, *subChannel, this->channel_->getName());
|
||||
twitchChannel_->localTwitchEmotes(),
|
||||
*getApp()->getAccounts()->twitch.getCurrent()->accessEmoteSets(),
|
||||
*globalChannel, *subChannel, twitchChannel_->roomId(),
|
||||
twitchChannel_->getName());
|
||||
|
||||
// global
|
||||
if (Settings::instance().enableBTTVGlobalEmotes)
|
||||
|
@ -475,24 +481,22 @@ bool EmotePopup::eventFilter(QObject *object, QEvent *event)
|
|||
void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
||||
const QString &searchText)
|
||||
{
|
||||
auto twitchEmoteSets =
|
||||
getApp()->getAccounts()->twitch.getCurrent()->accessEmotes()->emoteSets;
|
||||
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> twitchGlobalEmotes{};
|
||||
|
||||
for (const auto &set : twitchEmoteSets)
|
||||
if (this->twitchChannel_)
|
||||
{
|
||||
auto setCopy = std::make_shared<TwitchAccount::EmoteSet>(*set);
|
||||
auto setIt =
|
||||
std::remove_if(setCopy->emotes.begin(), setCopy->emotes.end(),
|
||||
[searchText](auto &emote) {
|
||||
return !emote.name.string.contains(
|
||||
searchText, Qt::CaseInsensitive);
|
||||
});
|
||||
setCopy->emotes.resize(std::distance(setCopy->emotes.begin(), setIt));
|
||||
|
||||
if (!setCopy->emotes.empty())
|
||||
auto local = filterEmoteMap(searchText,
|
||||
this->twitchChannel_->localTwitchEmotes());
|
||||
if (!local.empty())
|
||||
{
|
||||
twitchGlobalEmotes.push_back(setCopy);
|
||||
addEmotes(*searchChannel, local,
|
||||
this->twitchChannel_->getName() % u" (local)",
|
||||
MessageElementFlag::TwitchEmote);
|
||||
}
|
||||
|
||||
for (const auto &[_id, set] :
|
||||
**getApp()->getAccounts()->twitch.getCurrent()->accessEmoteSets())
|
||||
{
|
||||
addEmotes(*searchChannel, filterEmoteVec(searchText, set.emotes),
|
||||
set.title(), MessageElementFlag::TwitchEmote);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,10 +507,6 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
|||
auto seventvGlobalEmotes = filterEmoteMap(
|
||||
searchText, getApp()->getSeventvEmotes()->globalEmotes());
|
||||
|
||||
// twitch
|
||||
addTwitchEmoteSets(twitchGlobalEmotes, *searchChannel, *searchChannel,
|
||||
this->channel_->getName());
|
||||
|
||||
// global
|
||||
if (!bttvGlobalEmotes.empty())
|
||||
{
|
||||
|
|
|
@ -1508,10 +1508,10 @@ void Split::showSearch(bool singleChannel)
|
|||
void Split::reloadChannelAndSubscriberEmotes()
|
||||
{
|
||||
auto channel = this->getChannel();
|
||||
getApp()->getAccounts()->twitch.getCurrent()->loadEmotes(channel);
|
||||
|
||||
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||
{
|
||||
twitchChannel->refreshTwitchChannelEmotes(true);
|
||||
twitchChannel->refreshBTTVChannelEmotes(true);
|
||||
twitchChannel->refreshFFZChannelEmotes(true);
|
||||
twitchChannel->refreshSevenTVChannelEmotes(true);
|
||||
|
|
|
@ -1078,7 +1078,10 @@ void SplitHeader::reloadSubscriberEmotes()
|
|||
this->lastReloadedSubEmotes_ = now;
|
||||
|
||||
auto channel = this->split_->getChannel();
|
||||
getApp()->getAccounts()->twitch.getCurrent()->loadEmotes(channel);
|
||||
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||
{
|
||||
twitchChannel->refreshTwitchChannelEmotes(true);
|
||||
}
|
||||
}
|
||||
|
||||
void SplitHeader::reconnect()
|
||||
|
|
|
@ -47,6 +47,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
||||
|
|
173
tests/src/CancellationToken.cpp
Normal file
173
tests/src/CancellationToken.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
#include "util/CancellationToken.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
// CancellationToken
|
||||
|
||||
TEST(CancellationToken, ctor)
|
||||
{
|
||||
{
|
||||
CancellationToken token;
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
token.cancel();
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
token.cancel();
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
{
|
||||
CancellationToken token(true);
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
token.cancel();
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CancellationToken, moveCtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
CancellationToken token2(std::move(token));
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
|
||||
token.cancel();
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
|
||||
token2.cancel();
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
}
|
||||
|
||||
TEST(CancellationToken, moveAssign)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
CancellationToken token2;
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
|
||||
token2 = std::move(token);
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
token.cancel();
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
|
||||
token2.cancel();
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
}
|
||||
|
||||
TEST(CancellationToken, copyCtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
CancellationToken token2(token);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
token2.cancel();
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
|
||||
TEST(CancellationToken, copyAssign)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
CancellationToken token2;
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
|
||||
token2 = token;
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
token2.cancel();
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
}
|
||||
|
||||
TEST(CancellationToken, dtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
{
|
||||
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
|
||||
CancellationToken token2 = token;
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
}
|
||||
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
}
|
||||
|
||||
// ScopedCancellationToken
|
||||
|
||||
TEST(ScopedCancellationToken, moveCancelCtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
{
|
||||
ScopedCancellationToken scoped(std::move(token));
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move)
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ScopedCancellationToken, copyCancelCtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
{
|
||||
ScopedCancellationToken scoped(token);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
}
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
|
||||
TEST(ScopedCancellationToken, moveCtor)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
{
|
||||
ScopedCancellationToken scoped(token);
|
||||
{
|
||||
ScopedCancellationToken inner(std::move(scoped));
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
}
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ScopedCancellationToken, moveAssign)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
CancellationToken token2(false);
|
||||
{
|
||||
ScopedCancellationToken scoped(token);
|
||||
{
|
||||
ScopedCancellationToken inner(token2);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
inner = std::move(scoped);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
}
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ScopedCancellationToken, copyAssign)
|
||||
{
|
||||
CancellationToken token(false);
|
||||
CancellationToken token2(false);
|
||||
{
|
||||
ScopedCancellationToken scoped(token);
|
||||
ASSERT_FALSE(token.isCancelled());
|
||||
scoped = token2;
|
||||
ASSERT_FALSE(token2.isCancelled());
|
||||
ASSERT_TRUE(token.isCancelled());
|
||||
}
|
||||
ASSERT_TRUE(token2.isCancelled());
|
||||
}
|
Loading…
Reference in a new issue