mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Migrate Twitch badges to Helix (#4537)
Co-authored-by: iProdigy <8106344+iProdigy@users.noreply.github.com> Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
594477d8b6
commit
d6ef48d4ef
|
@ -12,6 +12,7 @@
|
||||||
- Minor: Reply context now censors blocked users. (#4502)
|
- Minor: Reply context now censors blocked users. (#4502)
|
||||||
- Minor: Added system message for empty mod list. (#4546)
|
- Minor: Added system message for empty mod list. (#4546)
|
||||||
- Minor: Added `/lowtrust` command to open the suspicious user activity feed in browser. (#4542)
|
- Minor: Added `/lowtrust` command to open the suspicious user activity feed in browser. (#4542)
|
||||||
|
- Minor: Migrated badges to Helix API. (#4537)
|
||||||
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
||||||
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
||||||
- Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460)
|
- Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "util/DisplayBadge.hpp"
|
#include "util/DisplayBadge.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
@ -29,25 +30,49 @@ void TwitchBadges::loadTwitchBadges()
|
||||||
{
|
{
|
||||||
assert(this->loaded_ == false);
|
assert(this->loaded_ == false);
|
||||||
|
|
||||||
QUrl url("https://badges.twitch.tv/v1/badges/global/display");
|
getHelix()->getGlobalBadges(
|
||||||
|
[this](auto globalBadges) {
|
||||||
|
auto badgeSets = this->badgeSets_.access();
|
||||||
|
|
||||||
QUrlQuery urlQuery;
|
for (const auto &badgeSet : globalBadges.badgeSets)
|
||||||
urlQuery.addQueryItem("language", "en");
|
{
|
||||||
url.setQuery(urlQuery);
|
const auto &setID = badgeSet.setID;
|
||||||
|
for (const auto &version : badgeSet.versions)
|
||||||
NetworkRequest(url)
|
{
|
||||||
.onSuccess([this](auto result) -> Outcome {
|
const auto &emote = Emote{
|
||||||
auto root = result.parseJson();
|
EmoteName{},
|
||||||
|
ImageSet{
|
||||||
this->parseTwitchBadges(root);
|
Image::fromUrl(version.imageURL1x, 1),
|
||||||
|
Image::fromUrl(version.imageURL2x, .5),
|
||||||
|
Image::fromUrl(version.imageURL4x, .25),
|
||||||
|
},
|
||||||
|
Tooltip{version.title},
|
||||||
|
version.clickURL,
|
||||||
|
};
|
||||||
|
(*badgeSets)[setID][version.id] =
|
||||||
|
std::make_shared<Emote>(emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this->loaded();
|
this->loaded();
|
||||||
return Success;
|
},
|
||||||
})
|
[this](auto error, auto message) {
|
||||||
.onError([this](auto res) {
|
QString errorMessage("Failed to load global badges - ");
|
||||||
qCWarning(chatterinoTwitch)
|
|
||||||
<< "Error loading Twitch Badges from the badges API:"
|
switch (error)
|
||||||
<< res.status() << " - falling back to backup";
|
{
|
||||||
|
case HelixGetGlobalBadgesError::Forwarded: {
|
||||||
|
errorMessage += message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// This would most likely happen if the service is down, or if the JSON payload returned has changed format
|
||||||
|
case HelixGetGlobalBadgesError::Unknown: {
|
||||||
|
errorMessage += "An unknown error has occurred.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qCWarning(chatterinoTwitch) << errorMessage;
|
||||||
QFile file(":/twitch-badges.json");
|
QFile file(":/twitch-badges.json");
|
||||||
if (!file.open(QFile::ReadOnly))
|
if (!file.open(QFile::ReadOnly))
|
||||||
{
|
{
|
||||||
|
@ -64,8 +89,7 @@ void TwitchBadges::loadTwitchBadges()
|
||||||
this->parseTwitchBadges(doc.object());
|
this->parseTwitchBadges(doc.object());
|
||||||
|
|
||||||
this->loaded();
|
this->loaded();
|
||||||
})
|
});
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
||||||
|
@ -93,7 +117,8 @@ void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
||||||
{versionObj.value("image_url_4x").toString()}, .25),
|
{versionObj.value("image_url_4x").toString()}, .25),
|
||||||
},
|
},
|
||||||
Tooltip{versionObj.value("title").toString()},
|
Tooltip{versionObj.value("title").toString()},
|
||||||
Url{versionObj.value("click_url").toString()}};
|
Url{versionObj.value("click_url").toString()},
|
||||||
|
};
|
||||||
// "title"
|
// "title"
|
||||||
// "clickAction"
|
// "clickAction"
|
||||||
|
|
||||||
|
|
|
@ -50,9 +50,6 @@ private:
|
||||||
|
|
||||||
TwitchBadges();
|
TwitchBadges();
|
||||||
void loadTwitchBadges();
|
void loadTwitchBadges();
|
||||||
/**
|
|
||||||
* @brief Accepts a JSON blob from https://badges.twitch.tv/v1/badges/global/display and updates our badges with it
|
|
||||||
**/
|
|
||||||
void parseTwitchBadges(QJsonObject root);
|
void parseTwitchBadges(QJsonObject root);
|
||||||
void loaded();
|
void loaded();
|
||||||
void loadEmoteImage(const QString &name, ImagePtr image,
|
void loadEmoteImage(const QString &name, ImagePtr image,
|
||||||
|
|
|
@ -1282,50 +1282,71 @@ void TwitchChannel::cleanUpReplyThreads()
|
||||||
|
|
||||||
void TwitchChannel::refreshBadges()
|
void TwitchChannel::refreshBadges()
|
||||||
{
|
{
|
||||||
auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" +
|
if (this->roomId().isEmpty())
|
||||||
this->roomId() + "/display?language=en"};
|
{
|
||||||
NetworkRequest(url.string)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
.onSuccess([this,
|
getHelix()->getChannelBadges(
|
||||||
weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
this->roomId(),
|
||||||
|
// successCallback
|
||||||
|
[this, weak = weakOf<Channel>(this)](auto channelBadges) {
|
||||||
auto shared = weak.lock();
|
auto shared = weak.lock();
|
||||||
if (!shared)
|
if (!shared)
|
||||||
return Failure;
|
{
|
||||||
|
// The channel has been closed inbetween us making the request and the request finishing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto badgeSets = this->badgeSets_.access();
|
auto badgeSets = this->badgeSets_.access();
|
||||||
|
|
||||||
auto jsonRoot = result.parseJson();
|
for (const auto &badgeSet : channelBadges.badgeSets)
|
||||||
|
|
||||||
auto _ = jsonRoot["badge_sets"].toObject();
|
|
||||||
for (auto jsonBadgeSet = _.begin(); jsonBadgeSet != _.end();
|
|
||||||
jsonBadgeSet++)
|
|
||||||
{
|
{
|
||||||
auto &versions = (*badgeSets)[jsonBadgeSet.key()];
|
const auto &setID = badgeSet.setID;
|
||||||
|
for (const auto &version : badgeSet.versions)
|
||||||
auto _set = jsonBadgeSet->toObject()["versions"].toObject();
|
|
||||||
for (auto jsonVersion_ = _set.begin();
|
|
||||||
jsonVersion_ != _set.end(); jsonVersion_++)
|
|
||||||
{
|
{
|
||||||
auto jsonVersion = jsonVersion_->toObject();
|
auto emote = Emote{
|
||||||
auto emote = std::make_shared<Emote>(Emote{
|
|
||||||
EmoteName{},
|
EmoteName{},
|
||||||
ImageSet{
|
ImageSet{
|
||||||
Image::fromUrl(
|
Image::fromUrl(version.imageURL1x, 1),
|
||||||
{jsonVersion["image_url_1x"].toString()}, 1),
|
Image::fromUrl(version.imageURL2x, .5),
|
||||||
Image::fromUrl(
|
Image::fromUrl(version.imageURL4x, .25),
|
||||||
{jsonVersion["image_url_2x"].toString()}, .5),
|
},
|
||||||
Image::fromUrl(
|
Tooltip{version.title},
|
||||||
{jsonVersion["image_url_4x"].toString()}, .25)},
|
version.clickURL,
|
||||||
Tooltip{jsonVersion["description"].toString()},
|
};
|
||||||
Url{jsonVersion["clickURL"].toString()}});
|
(*badgeSets)[setID][version.id] =
|
||||||
|
std::make_shared<Emote>(emote);
|
||||||
versions.emplace(jsonVersion_.key(), emote);
|
}
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
// failureCallback
|
||||||
|
[this, weak = weakOf<Channel>(this)](auto error, auto message) {
|
||||||
|
auto shared = weak.lock();
|
||||||
|
if (!shared)
|
||||||
|
{
|
||||||
|
// The channel has been closed inbetween us making the request and the request finishing
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Success;
|
QString errorMessage("Failed to load channel badges - ");
|
||||||
})
|
|
||||||
.execute();
|
switch (error)
|
||||||
|
{
|
||||||
|
case HelixGetChannelBadgesError::Forwarded: {
|
||||||
|
errorMessage += message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// This would most likely happen if the service is down, or if the JSON payload returned has changed format
|
||||||
|
case HelixGetChannelBadgesError::Unknown: {
|
||||||
|
errorMessage += "An unknown error has occurred.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->addMessage(makeSystemMessage(errorMessage));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchChannel::refreshCheerEmotes()
|
void TwitchChannel::refreshCheerEmotes()
|
||||||
|
|
|
@ -2468,6 +2468,98 @@ void Helix::startCommercial(
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Twitch global badges
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||||
|
void Helix::getGlobalBadges(
|
||||||
|
ResultCallback<HelixGlobalBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetGlobalBadgesError, QString> failureCallback)
|
||||||
|
{
|
||||||
|
using Error = HelixGetGlobalBadgesError;
|
||||||
|
|
||||||
|
this->makeRequest("chat/badges/global", QUrlQuery())
|
||||||
|
.onSuccess([successCallback](auto result) -> Outcome {
|
||||||
|
if (result.status() != 200)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Success result for getting global badges was "
|
||||||
|
<< result.status() << "but we expected it to be 200";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = result.parseJson();
|
||||||
|
successCallback(HelixGlobalBadges(response));
|
||||||
|
return Success;
|
||||||
|
})
|
||||||
|
.onError([failureCallback](auto result) {
|
||||||
|
auto obj = result.parseJson();
|
||||||
|
auto message = obj.value("message").toString();
|
||||||
|
|
||||||
|
switch (result.status())
|
||||||
|
{
|
||||||
|
case 401: {
|
||||||
|
failureCallback(Error::Forwarded, message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Helix global badges, unhandled error data:"
|
||||||
|
<< result.status() << result.getData() << obj;
|
||||||
|
failureCallback(Error::Unknown, message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badges for the `broadcasterID` channel
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||||
|
void Helix::getChannelBadges(
|
||||||
|
QString broadcasterID, ResultCallback<HelixChannelBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetChannelBadgesError, QString> failureCallback)
|
||||||
|
{
|
||||||
|
using Error = HelixGetChannelBadgesError;
|
||||||
|
|
||||||
|
QUrlQuery urlQuery;
|
||||||
|
urlQuery.addQueryItem("broadcaster_id", broadcasterID);
|
||||||
|
|
||||||
|
this->makeRequest("chat/badges", urlQuery)
|
||||||
|
.onSuccess([successCallback](auto result) -> Outcome {
|
||||||
|
if (result.status() != 200)
|
||||||
|
{
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Success result for getting badges was "
|
||||||
|
<< result.status() << "but we expected it to be 200";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = result.parseJson();
|
||||||
|
successCallback(HelixChannelBadges(response));
|
||||||
|
return Success;
|
||||||
|
})
|
||||||
|
.onError([failureCallback](auto result) {
|
||||||
|
auto obj = result.parseJson();
|
||||||
|
auto message = obj.value("message").toString();
|
||||||
|
|
||||||
|
switch (result.status())
|
||||||
|
{
|
||||||
|
case 400:
|
||||||
|
case 401: {
|
||||||
|
failureCallback(Error::Forwarded, message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
qCWarning(chatterinoTwitch)
|
||||||
|
<< "Helix channel badges, unhandled error data:"
|
||||||
|
<< result.status() << result.getData() << obj;
|
||||||
|
failureCallback(Error::Unknown, message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
|
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
|
||||||
{
|
{
|
||||||
assert(!url.startsWith("/"));
|
assert(!url.startsWith("/"));
|
||||||
|
|
|
@ -384,6 +384,55 @@ struct HelixModerators {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct HelixBadgeVersion {
|
||||||
|
QString id;
|
||||||
|
Url imageURL1x;
|
||||||
|
Url imageURL2x;
|
||||||
|
Url imageURL4x;
|
||||||
|
QString title;
|
||||||
|
Url clickURL;
|
||||||
|
|
||||||
|
explicit HelixBadgeVersion(const QJsonObject &jsonObject)
|
||||||
|
: id(jsonObject.value("id").toString())
|
||||||
|
, imageURL1x(Url{jsonObject.value("image_url_1x").toString()})
|
||||||
|
, imageURL2x(Url{jsonObject.value("image_url_2x").toString()})
|
||||||
|
, imageURL4x(Url{jsonObject.value("image_url_4x").toString()})
|
||||||
|
, title(jsonObject.value("title").toString())
|
||||||
|
, clickURL(Url{jsonObject.value("click_url").toString()})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HelixBadgeSet {
|
||||||
|
QString setID;
|
||||||
|
std::vector<HelixBadgeVersion> versions;
|
||||||
|
|
||||||
|
explicit HelixBadgeSet(const QJsonObject &json)
|
||||||
|
: setID(json.value("set_id").toString())
|
||||||
|
{
|
||||||
|
const auto jsonVersions = json.value("versions").toArray();
|
||||||
|
for (const auto &version : jsonVersions)
|
||||||
|
{
|
||||||
|
versions.emplace_back(version.toObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HelixGlobalBadges {
|
||||||
|
std::vector<HelixBadgeSet> badgeSets;
|
||||||
|
|
||||||
|
explicit HelixGlobalBadges(const QJsonObject &jsonObject)
|
||||||
|
{
|
||||||
|
const auto &data = jsonObject.value("data").toArray();
|
||||||
|
for (const auto &set : data)
|
||||||
|
{
|
||||||
|
this->badgeSets.emplace_back(set.toObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using HelixChannelBadges = HelixGlobalBadges;
|
||||||
|
|
||||||
enum class HelixAnnouncementColor {
|
enum class HelixAnnouncementColor {
|
||||||
Blue,
|
Blue,
|
||||||
Green,
|
Green,
|
||||||
|
@ -616,6 +665,15 @@ enum class HelixStartCommercialError {
|
||||||
Forwarded,
|
Forwarded,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class HelixGetGlobalBadgesError {
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
// The error message is forwarded directly from the Twitch API
|
||||||
|
Forwarded,
|
||||||
|
};
|
||||||
|
|
||||||
|
using HelixGetChannelBadgesError = HelixGetGlobalBadgesError;
|
||||||
|
|
||||||
class IHelix
|
class IHelix
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -899,6 +957,21 @@ public:
|
||||||
FailureCallback<HelixStartCommercialError, QString>
|
FailureCallback<HelixStartCommercialError, QString>
|
||||||
failureCallback) = 0;
|
failureCallback) = 0;
|
||||||
|
|
||||||
|
// Get global Twitch badges
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||||
|
virtual void getGlobalBadges(
|
||||||
|
ResultCallback<HelixGlobalBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetGlobalBadgesError, QString>
|
||||||
|
failureCallback) = 0;
|
||||||
|
|
||||||
|
// Get badges for the `broadcasterID` channel
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||||
|
virtual void getChannelBadges(
|
||||||
|
QString broadcasterID,
|
||||||
|
ResultCallback<HelixChannelBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetChannelBadgesError, QString>
|
||||||
|
failureCallback) = 0;
|
||||||
|
|
||||||
virtual void update(QString clientId, QString oauthToken) = 0;
|
virtual void update(QString clientId, QString oauthToken) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -1184,6 +1257,19 @@ public:
|
||||||
FailureCallback<HelixStartCommercialError, QString> failureCallback)
|
FailureCallback<HelixStartCommercialError, QString> failureCallback)
|
||||||
final;
|
final;
|
||||||
|
|
||||||
|
// Get global Twitch badges
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||||
|
void getGlobalBadges(ResultCallback<HelixGlobalBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetGlobalBadgesError, QString>
|
||||||
|
failureCallback) final;
|
||||||
|
|
||||||
|
// Get badges for the `broadcasterID` channel
|
||||||
|
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||||
|
void getChannelBadges(QString broadcasterID,
|
||||||
|
ResultCallback<HelixChannelBadges> successCallback,
|
||||||
|
FailureCallback<HelixGetChannelBadgesError, QString>
|
||||||
|
failureCallback) final;
|
||||||
|
|
||||||
void update(QString clientId, QString oauthToken) final;
|
void update(QString clientId, QString oauthToken) final;
|
||||||
|
|
||||||
static void initialize();
|
static void initialize();
|
||||||
|
|
|
@ -136,6 +136,22 @@ Used in:
|
||||||
|
|
||||||
- `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
- `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
||||||
|
|
||||||
|
### Get Global Badges
|
||||||
|
|
||||||
|
URL: https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||||
|
|
||||||
|
Used in:
|
||||||
|
|
||||||
|
- `providers/twitch/TwitchBadges.cpp` to load global badges
|
||||||
|
|
||||||
|
### Get Channel Badges
|
||||||
|
|
||||||
|
URL: https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||||
|
|
||||||
|
Used in:
|
||||||
|
|
||||||
|
- `providers/twitch/TwitchChannel.cpp` to load channel badges
|
||||||
|
|
||||||
### Get Emote Sets
|
### Get Emote Sets
|
||||||
|
|
||||||
URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets
|
URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets
|
||||||
|
|
|
@ -225,6 +225,21 @@ public:
|
||||||
HelixFailureCallback failureCallback),
|
HelixFailureCallback failureCallback),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||||
|
MOCK_METHOD(
|
||||||
|
void, getGlobalBadges,
|
||||||
|
(ResultCallback<HelixGlobalBadges> successCallback,
|
||||||
|
(FailureCallback<HelixGetGlobalBadgesError, QString> failureCallback)),
|
||||||
|
(override));
|
||||||
|
|
||||||
|
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||||
|
MOCK_METHOD(void, getChannelBadges,
|
||||||
|
(QString broadcasterID,
|
||||||
|
ResultCallback<HelixChannelBadges> successCallback,
|
||||||
|
(FailureCallback<HelixGetChannelBadgesError, QString>
|
||||||
|
failureCallback)),
|
||||||
|
(override));
|
||||||
|
|
||||||
// The extra parenthesis around the failure callback is because its type contains a comma
|
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||||
MOCK_METHOD(void, updateUserChatColor,
|
MOCK_METHOD(void, updateUserChatColor,
|
||||||
(QString userID, QString color,
|
(QString userID, QString color,
|
||||||
|
|
Loading…
Reference in a new issue