mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
badge texts wip
This commit is contained in:
parent
c4c62f2796
commit
7908e8855f
|
@ -24,6 +24,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
* List of identifiers:
|
* List of identifiers:
|
||||||
*
|
*
|
||||||
* author.badges
|
* author.badges
|
||||||
|
* author.badge_texts
|
||||||
* author.color
|
* author.color
|
||||||
* author.name
|
* author.name
|
||||||
* author.no_color
|
* author.no_color
|
||||||
|
@ -56,11 +57,16 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
|
|
||||||
QStringList badges;
|
QStringList badges;
|
||||||
badges.reserve(m->badges.size());
|
badges.reserve(m->badges.size());
|
||||||
|
QStringList badgeTexts;
|
||||||
|
badgeTexts.reserve(m->badges.size());
|
||||||
for (const auto &e : m->badges)
|
for (const auto &e : m->badges)
|
||||||
{
|
{
|
||||||
badges << e.key_;
|
badges << e.key;
|
||||||
|
badgeTexts << e.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qDebug() << "XXX: Badges:" << badges << badgeTexts;
|
||||||
|
|
||||||
bool watching = !watchingChannel->getName().isEmpty() &&
|
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||||
watchingChannel->getName().compare(
|
watchingChannel->getName().compare(
|
||||||
m->channelName, Qt::CaseInsensitive) == 0;
|
m->channelName, Qt::CaseInsensitive) == 0;
|
||||||
|
@ -81,6 +87,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
}
|
}
|
||||||
ContextMap vars = {
|
ContextMap vars = {
|
||||||
{"author.badges", std::move(badges)},
|
{"author.badges", std::move(badges)},
|
||||||
|
{"author.badge_texts", std::move(badgeTexts)},
|
||||||
{"author.color", m->usernameColor},
|
{"author.color", m->usernameColor},
|
||||||
{"author.name", m->displayName},
|
{"author.name", m->displayName},
|
||||||
{"author.no_color", !m->usernameColor.isValid()},
|
{"author.no_color", !m->usernameColor.isValid()},
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace chatterino::filters {
|
||||||
// i.e. if all the variables and operators being used have compatible types.
|
// i.e. if all the variables and operators being used have compatible types.
|
||||||
static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
|
static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
|
||||||
{"author.badges", Type::StringList},
|
{"author.badges", Type::StringList},
|
||||||
|
{"author.badge_texts", Type::StringList},
|
||||||
{"author.color", Type::Color},
|
{"author.color", Type::Color},
|
||||||
{"author.name", Type::String},
|
{"author.name", Type::String},
|
||||||
{"author.no_color", Type::Bool},
|
{"author.no_color", Type::Bool},
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace chatterino::filters {
|
||||||
|
|
||||||
static const QMap<QString, QString> validIdentifiersMap = {
|
static const QMap<QString, QString> validIdentifiersMap = {
|
||||||
{"author.badges", "author badges"},
|
{"author.badges", "author badges"},
|
||||||
|
{"author.badge_texts", "author badge texts"},
|
||||||
{"author.color", "author color"},
|
{"author.color", "author color"},
|
||||||
{"author.name", "author name"},
|
{"author.name", "author name"},
|
||||||
{"author.no_color", "author has no color?"},
|
{"author.no_color", "author has no color?"},
|
||||||
|
@ -35,7 +36,8 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
||||||
{"flags.restricted", "restricted message?"},
|
{"flags.restricted", "restricted message?"},
|
||||||
{"flags.monitored", "monitored message?"},
|
{"flags.monitored", "monitored message?"},
|
||||||
{"message.content", "message text"},
|
{"message.content", "message text"},
|
||||||
{"message.length", "message length"}};
|
{"message.length", "message length"},
|
||||||
|
};
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const QRegularExpression tokenRegex(
|
static const QRegularExpression tokenRegex(
|
||||||
|
|
|
@ -98,11 +98,11 @@ bool HighlightBadge::compare(const QString &id, const Badge &badge) const
|
||||||
if (this->hasVersions_)
|
if (this->hasVersions_)
|
||||||
{
|
{
|
||||||
auto parts = SharedMessageBuilder::slashKeyValue(id);
|
auto parts = SharedMessageBuilder::slashKeyValue(id);
|
||||||
return parts.first.compare(badge.key_, Qt::CaseInsensitive) == 0 &&
|
return parts.first.compare(badge.key, Qt::CaseInsensitive) == 0 &&
|
||||||
parts.second.compare(badge.value_, Qt::CaseInsensitive) == 0;
|
parts.second.compare(badge.value, Qt::CaseInsensitive) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return id.compare(badge.key_, Qt::CaseInsensitive) == 0;
|
return id.compare(badge.key, Qt::CaseInsensitive) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HighlightBadge::hasCustomSound() const
|
bool HighlightBadge::hasCustomSound() const
|
||||||
|
|
|
@ -36,7 +36,7 @@ bool BadgePredicate::appliesToImpl(const Message &message)
|
||||||
{
|
{
|
||||||
for (const Badge &badge : message.badges)
|
for (const Badge &badge : message.badges)
|
||||||
{
|
{
|
||||||
if (badges_.contains(badge.key_, Qt::CaseInsensitive))
|
if (badges_.contains(badge.key, Qt::CaseInsensitive))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,10 @@ bool SubtierPredicate::appliesToImpl(const Message &message)
|
||||||
{
|
{
|
||||||
for (const Badge &badge : message.badges)
|
for (const Badge &badge : message.badges)
|
||||||
{
|
{
|
||||||
if (badge.key_ == "subscriber")
|
if (badge.key == "subscriber")
|
||||||
{
|
{
|
||||||
const auto &subTier =
|
const auto &subTier =
|
||||||
badge.value_.length() > 3 ? badge.value_.at(0) : '1';
|
badge.value.length() > 3 ? badge.value.at(0) : '1';
|
||||||
|
|
||||||
return subtiers_.contains(subTier);
|
return subtiers_.contains(subTier);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,30 +12,30 @@ const QSet<QString> channelAuthority{"moderator", "vip", "broadcaster"};
|
||||||
const QSet<QString> subBadges{"subscriber", "founder"};
|
const QSet<QString> subBadges{"subscriber", "founder"};
|
||||||
|
|
||||||
Badge::Badge(QString key, QString value)
|
Badge::Badge(QString key, QString value)
|
||||||
: key_(std::move(key))
|
: key(std::move(key))
|
||||||
, value_(std::move(value))
|
, value(std::move(value))
|
||||||
{
|
{
|
||||||
if (globalAuthority.contains(this->key_))
|
if (globalAuthority.contains(this->key))
|
||||||
{
|
{
|
||||||
this->flag_ = MessageElementFlag::BadgeGlobalAuthority;
|
this->flag = MessageElementFlag::BadgeGlobalAuthority;
|
||||||
}
|
}
|
||||||
else if (predictions.contains(this->key_))
|
else if (predictions.contains(this->key))
|
||||||
{
|
{
|
||||||
this->flag_ = MessageElementFlag::BadgePredictions;
|
this->flag = MessageElementFlag::BadgePredictions;
|
||||||
}
|
}
|
||||||
else if (channelAuthority.contains(this->key_))
|
else if (channelAuthority.contains(this->key))
|
||||||
{
|
{
|
||||||
this->flag_ = MessageElementFlag::BadgeChannelAuthority;
|
this->flag = MessageElementFlag::BadgeChannelAuthority;
|
||||||
}
|
}
|
||||||
else if (subBadges.contains(this->key_))
|
else if (subBadges.contains(this->key))
|
||||||
{
|
{
|
||||||
this->flag_ = MessageElementFlag::BadgeSubscription;
|
this->flag = MessageElementFlag::BadgeSubscription;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Badge::operator==(const Badge &other) const
|
bool Badge::operator==(const Badge &other) const
|
||||||
{
|
{
|
||||||
return this->key_ == other.key_ && this->value_ == other.value_;
|
return this->key == other.key && this->value == other.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -4,8 +4,14 @@
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct Emote;
|
||||||
|
using EmotePtr = std::shared_ptr<const Emote>;
|
||||||
|
|
||||||
class Badge
|
class Badge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -13,13 +19,42 @@ public:
|
||||||
|
|
||||||
bool operator==(const Badge &other) const;
|
bool operator==(const Badge &other) const;
|
||||||
|
|
||||||
// Class members are fetched from both "badges" and "badge-info" tags
|
/**
|
||||||
// E.g.: "badges": "subscriber/18", "badge-info": "subscriber/22"
|
* The key of the badge (e.g. subscriber, moderator, chatter-cs-go-2022)
|
||||||
QString key_; // subscriber
|
*/
|
||||||
QString value_; // 18
|
QString key; // subscriber
|
||||||
//QString info_; // 22 (should be parsed separetly into an std::unordered_map)
|
//
|
||||||
MessageElementFlag flag_{
|
/**
|
||||||
MessageElementFlag::BadgeVanity}; // badge slot it takes up
|
* The value of the badge (e.g. 96 for a subscriber badge, denoting that this should use the 96-month sub badge)
|
||||||
|
*/
|
||||||
|
QString value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text of the badge
|
||||||
|
* By default, the text is empty & will be filled in separately if text is found
|
||||||
|
* The text is what will be displayed in the badge's tooltip
|
||||||
|
*/
|
||||||
|
QString text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The image of the badge
|
||||||
|
* Can be nullopt if the badge just doesn't have an image, or if no image has been found set it yet
|
||||||
|
*/
|
||||||
|
std::optional<EmotePtr> image{};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The badge slot this badge takes up
|
||||||
|
*/
|
||||||
|
MessageElementFlag flag{MessageElementFlag::BadgeVanity};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
||||||
|
inline QDebug operator<<(QDebug debug, const chatterino::Badge &v)
|
||||||
|
{
|
||||||
|
QDebugStateSaver saver(debug);
|
||||||
|
debug.nospace() << "(key=" << v.key << ", value=" << v.value
|
||||||
|
<< ", text=" << v.text << ')';
|
||||||
|
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
|
|
@ -157,13 +157,13 @@ namespace {
|
||||||
const TwitchChannel *twitchChannel)
|
const TwitchChannel *twitchChannel)
|
||||||
{
|
{
|
||||||
if (auto channelBadge =
|
if (auto channelBadge =
|
||||||
twitchChannel->twitchBadge(badge.key_, badge.value_))
|
twitchChannel->twitchBadge(badge.key, badge.value_))
|
||||||
{
|
{
|
||||||
return channelBadge;
|
return channelBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto globalBadge =
|
if (auto globalBadge =
|
||||||
TwitchBadges::instance()->badge(badge.key_, badge.value_))
|
TwitchBadges::instance()->badge(badge.key, badge.value_))
|
||||||
{
|
{
|
||||||
return globalBadge;
|
return globalBadge;
|
||||||
}
|
}
|
||||||
|
@ -182,19 +182,12 @@ namespace {
|
||||||
|
|
||||||
for (const auto &badge : badges)
|
for (const auto &badge : badges)
|
||||||
{
|
{
|
||||||
auto badgeEmote = getTwitchBadge(badge, twitchChannel);
|
if (!badge.image)
|
||||||
if (!badgeEmote)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto tooltip = (*badgeEmote)->tooltip.string;
|
|
||||||
|
|
||||||
if (badge.key_ == "bits")
|
if (badge.key == "moderator" &&
|
||||||
{
|
|
||||||
const auto &cheerAmount = badge.value_;
|
|
||||||
tooltip = QString("Twitch cheer %0").arg(cheerAmount);
|
|
||||||
}
|
|
||||||
else if (badge.key_ == "moderator" &&
|
|
||||||
getSettings()->useCustomFfzModeratorBadges)
|
getSettings()->useCustomFfzModeratorBadges)
|
||||||
{
|
{
|
||||||
if (auto customModBadge = twitchChannel->ffzCustomModBadge())
|
if (auto customModBadge = twitchChannel->ffzCustomModBadge())
|
||||||
|
@ -203,13 +196,12 @@ namespace {
|
||||||
->emplace<ModBadgeElement>(
|
->emplace<ModBadgeElement>(
|
||||||
*customModBadge,
|
*customModBadge,
|
||||||
MessageElementFlag::BadgeChannelAuthority)
|
MessageElementFlag::BadgeChannelAuthority)
|
||||||
->setTooltip((*customModBadge)->tooltip.string);
|
->setTooltip(badge.text);
|
||||||
// early out, since we have to add a custom badge element here
|
// early out, since we have to add a custom badge element here
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (badge.key_ == "vip" &&
|
else if (badge.key == "vip" && getSettings()->useCustomFfzVipBadges)
|
||||||
getSettings()->useCustomFfzVipBadges)
|
|
||||||
{
|
{
|
||||||
if (auto customVipBadge = twitchChannel->ffzCustomVipBadge())
|
if (auto customVipBadge = twitchChannel->ffzCustomVipBadge())
|
||||||
{
|
{
|
||||||
|
@ -217,49 +209,14 @@ namespace {
|
||||||
->emplace<VipBadgeElement>(
|
->emplace<VipBadgeElement>(
|
||||||
*customVipBadge,
|
*customVipBadge,
|
||||||
MessageElementFlag::BadgeChannelAuthority)
|
MessageElementFlag::BadgeChannelAuthority)
|
||||||
->setTooltip((*customVipBadge)->tooltip.string);
|
->setTooltip(badge.text);
|
||||||
// early out, since we have to add a custom badge element here
|
// early out, since we have to add a custom badge element here
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (badge.flag_ == MessageElementFlag::BadgeSubscription)
|
|
||||||
{
|
|
||||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
|
||||||
if (badgeInfoIt != badgeInfos.end())
|
|
||||||
{
|
|
||||||
// badge.value_ is 4 chars long if user is subbed on higher tier
|
|
||||||
// (tier + amount of months with leading zero if less than 100)
|
|
||||||
// e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub
|
|
||||||
const auto &subTier =
|
|
||||||
badge.value_.length() > 3 ? badge.value_.at(0) : '1';
|
|
||||||
const auto &subMonths = badgeInfoIt->second;
|
|
||||||
tooltip += QString(" (%1%2 months)")
|
|
||||||
.arg(subTier != '1'
|
|
||||||
? QString("Tier %1, ").arg(subTier)
|
|
||||||
: "")
|
|
||||||
.arg(subMonths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (badge.flag_ == MessageElementFlag::BadgePredictions)
|
|
||||||
{
|
|
||||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
|
||||||
if (badgeInfoIt != badgeInfos.end())
|
|
||||||
{
|
|
||||||
auto infoValue = badgeInfoIt->second;
|
|
||||||
auto predictionText =
|
|
||||||
infoValue
|
|
||||||
.replace(R"(\s)", " ") // standard IRC escapes
|
|
||||||
.replace(R"(\:)", ";")
|
|
||||||
.replace(R"(\\)", R"(\)")
|
|
||||||
.replace("⸝", ","); // twitch's comma escape
|
|
||||||
// Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma
|
|
||||||
|
|
||||||
tooltip = QString("Predicted %1").arg(predictionText);
|
builder->emplace<BadgeElement>(*badge.image, badge.flag)
|
||||||
}
|
->setTooltip(badge.text);
|
||||||
}
|
|
||||||
|
|
||||||
builder->emplace<BadgeElement>(*badgeEmote, badge.flag_)
|
|
||||||
->setTooltip(tooltip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder->message().badges = badges;
|
builder->message().badges = badges;
|
||||||
|
@ -1285,6 +1242,62 @@ void TwitchMessageBuilder::appendTwitchBadges()
|
||||||
|
|
||||||
auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags);
|
auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags);
|
||||||
auto badges = TwitchMessageBuilder::parseBadgeTag(this->tags);
|
auto badges = TwitchMessageBuilder::parseBadgeTag(this->tags);
|
||||||
|
for (auto &badge : badges)
|
||||||
|
{
|
||||||
|
auto oBadgeEmote = getTwitchBadge(badge, this->twitchChannel);
|
||||||
|
if (!oBadgeEmote.has_value())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &badgeEmote = oBadgeEmote.value();
|
||||||
|
badge.image = badgeEmote;
|
||||||
|
|
||||||
|
if (badge.key == "bits")
|
||||||
|
{
|
||||||
|
const auto &cheerAmount = badge.value;
|
||||||
|
badge.text = QString("Twitch cheer %0").arg(cheerAmount);
|
||||||
|
}
|
||||||
|
else if (badge.flag == MessageElementFlag::BadgeSubscription)
|
||||||
|
{
|
||||||
|
auto badgeInfoIt = badgeInfos.find(badge.key);
|
||||||
|
if (badgeInfoIt != badgeInfos.end())
|
||||||
|
{
|
||||||
|
// badge.value_ is 4 chars long if user is subbed on higher tier
|
||||||
|
// (tier + amount of months with leading zero if less than 100)
|
||||||
|
// e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub
|
||||||
|
const auto &subTier =
|
||||||
|
badge.value.length() > 3 ? badge.value.at(0) : '1';
|
||||||
|
const auto &subMonths = badgeInfoIt->second;
|
||||||
|
badge.text =
|
||||||
|
QString("%0 (%1%2 months)")
|
||||||
|
.arg(badgeEmote->tooltip.string)
|
||||||
|
.arg(subTier != '1' ? QString("Tier %1, ").arg(subTier)
|
||||||
|
: "")
|
||||||
|
.arg(subMonths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (badge.flag == MessageElementFlag::BadgePredictions)
|
||||||
|
{
|
||||||
|
auto badgeInfoIt = badgeInfos.find(badge.key);
|
||||||
|
if (badgeInfoIt != badgeInfos.end())
|
||||||
|
{
|
||||||
|
auto infoValue = badgeInfoIt->second;
|
||||||
|
auto predictionText =
|
||||||
|
infoValue
|
||||||
|
.replace(R"(\s)", " ") // standard IRC escapes
|
||||||
|
.replace(R"(\:)", ";")
|
||||||
|
.replace(R"(\\)", R"(\)")
|
||||||
|
.replace("⸝", ","); // twitch's comma escape
|
||||||
|
// Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma
|
||||||
|
|
||||||
|
badge.text = QString("Predicted %1").arg(predictionText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
badge.text = badgeEmote->tooltip.string;
|
||||||
|
}
|
||||||
|
}
|
||||||
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -193,7 +193,19 @@ TEST(Filters, Evaluation)
|
||||||
{"author.subbed", QVariant(false)},
|
{"author.subbed", QVariant(false)},
|
||||||
{"message.content", QVariant("hey there :) 2038-01-19 123 456")},
|
{"message.content", QVariant("hey there :) 2038-01-19 123 456")},
|
||||||
{"channel.name", QVariant("forsen")},
|
{"channel.name", QVariant("forsen")},
|
||||||
{"author.badges", QVariant(QStringList({"moderator", "staff"}))}};
|
{"author.badges",
|
||||||
|
QStringList{
|
||||||
|
"moderator",
|
||||||
|
"staff",
|
||||||
|
"premium",
|
||||||
|
}},
|
||||||
|
{"author.badge_texts",
|
||||||
|
QStringList{
|
||||||
|
"Moderator",
|
||||||
|
"Staff",
|
||||||
|
"Prime Gaming",
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
std::vector<TestCase> tests
|
std::vector<TestCase> tests
|
||||||
|
@ -234,6 +246,10 @@ TEST(Filters, Evaluation)
|
||||||
{R".(!author.subbed).", QVariant(true)},
|
{R".(!author.subbed).", QVariant(true)},
|
||||||
{R".(author.color == "#ff0000").", QVariant(true)},
|
{R".(author.color == "#ff0000").", QVariant(true)},
|
||||||
{R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)},
|
{R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)},
|
||||||
|
{R".(author.badges contains "moderator").", QVariant(true)},
|
||||||
|
{R".(author.badge_texts contains "Moderator").", QVariant(true)},
|
||||||
|
{R".(author.badge_texts contains "Subscriber").", QVariant(false)},
|
||||||
|
{R".(author.badge_texts contains "Prime Gaming").", QVariant(true)},
|
||||||
{R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")},
|
{R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")},
|
||||||
{R".(message.content match r"HEY THERE").", QVariant(false)},
|
{R".(message.content match r"HEY THERE").", QVariant(false)},
|
||||||
{R".(message.content match ri"HEY THERE").", QVariant(true)},
|
{R".(message.content match ri"HEY THERE").", QVariant(true)},
|
||||||
|
|
Loading…
Reference in a new issue