badge texts wip

This commit is contained in:
Rasmus Karlsson 2024-01-28 09:12:43 +01:00
parent c4c62f2796
commit 7908e8855f
10 changed files with 154 additions and 80 deletions

View file

@ -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()},

View file

@ -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},

View file

@ -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(

View file

@ -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

View file

@ -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;
} }

View file

@ -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);
} }

View file

@ -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

View file

@ -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;
}

View file

@ -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);
} }

View file

@ -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)},