fix: correctly override user color in sub gifts (#6322)

This commit is contained in:
pajlada
2025-07-06 15:05:28 +02:00
committed by GitHub
parent f2d7e4d073
commit 7f2168a9d4
14 changed files with 410 additions and 12 deletions
+1
View File
@@ -31,6 +31,7 @@
- Bugfix: Fixed blocked users showing up in "Users joined:" and "Users parted:" messages. (#6181)
- Bugfix: Fixed an issue where text boxes in the settings dialog could be stuck with an old value. (#6286)
- Bugfix: Fixed an issue where Splits could get lost by dragging it onto your Recycle Bin. (#6147)
- Bugfix: Correctly color gifter & recipient usernames in subscription gift messages, taking all color sources into consideration. (#6322)
- Bugfix: Fixed some Twitch commands not getting tab-completed correctly. (#6143)
- Bugfix: Fixed shared chat badges displaying pixelated when Chatterino is scaled too much. (#6146)
- Bugfix: Fixed a few crashes that could occur when Chatterino was shutting down, some related to network tasks still firing despite us shutting down. (#6187)
+2
View File
@@ -440,6 +440,8 @@ set(SOURCE_FILES
providers/twitch/TwitchUser.hpp
providers/twitch/TwitchUsers.cpp
providers/twitch/TwitchUsers.hpp
providers/twitch/UserColor.cpp
providers/twitch/UserColor.hpp
providers/twitch/eventsub/Connection.cpp
providers/twitch/eventsub/Connection.hpp
+1 -1
View File
@@ -112,7 +112,7 @@ size_t ChannelChatters::colorsSize() const
return size;
}
const QColor ChannelChatters::getUserColor(const QString &user)
QColor ChannelChatters::getUserColor(const QString &user) const
{
const auto chatterColors = this->chatterColors_.access();
+1 -1
View File
@@ -24,7 +24,7 @@ public:
void addRecentChatter(const QString &user);
void addJoinedUser(const QString &user, bool isMod, bool isBroadcaster);
void addPartedUser(const QString &user, bool isMod, bool isBroadcaster);
const QColor getUserColor(const QString &user);
QColor getUserColor(const QString &user) const;
void setUserColor(const QString &user, const QColor &color);
void updateOnlineChatters(const std::unordered_set<QString> &usernames);
+28 -8
View File
@@ -32,6 +32,7 @@
#include "providers/twitch/TwitchIrc.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchUsers.hpp"
#include "providers/twitch/UserColor.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
@@ -587,8 +588,12 @@ MessagePtrMut MessageBuilder::makeSystemMessageWithUser(
MessagePtrMut MessageBuilder::makeSubgiftMessage(const QString &text,
const QVariantMap &tags,
const QTime &time)
const QTime &time,
TwitchChannel *channel)
{
const auto *userDataController = getApp()->getUserData();
assert(userDataController != nullptr);
MessageBuilder builder;
builder.emplace<TimestampElement>(time);
@@ -598,11 +603,16 @@ MessagePtrMut MessageBuilder::makeSubgiftMessage(const QString &text,
{
gifterDisplayName = gifterLogin;
}
MessageColor gifterColor = MessageColor::System;
if (auto colorTag = tags.value("color").value<QColor>(); colorTag.isValid())
{
gifterColor = MessageColor(colorTag);
}
auto gifterColor =
twitch::getUserColor({
.userLogin = gifterLogin,
.userID = tags.value("user-id").toString(),
.userDataController = userDataController,
.channelChatters = channel,
.color = tags.value("color").value<QColor>(),
})
.value_or(MessageColor::System);
auto recipientLogin =
tags.value("msg-param-recipient-user-name").toString();
@@ -617,6 +627,17 @@ MessagePtrMut MessageBuilder::makeSubgiftMessage(const QString &text,
recipientDisplayName = recipientLogin;
}
auto recipientColor =
twitch::getUserColor(
{
.userLogin = recipientLogin,
.userID = tags.value("msg-param-recipient-id").toString(),
.userDataController = userDataController,
.channelChatters = channel,
})
.value_or(MessageColor::System);
const auto textFragments = text.split(SPACE_REGEX, Qt::SkipEmptyParts);
for (const auto &word : textFragments)
{
@@ -632,8 +653,7 @@ MessagePtrMut MessageBuilder::makeSubgiftMessage(const QString &text,
{
builder
.emplace<MentionElement>(recipientDisplayName, recipientLogin,
MessageColor::System,
MessageColor::System)
MessageColor::System, recipientColor)
->setTrailingSpace(false);
builder.emplace<TextElement>(u"!"_s, MessageElementFlag::Text,
MessageColor::System);
+2 -1
View File
@@ -240,7 +240,8 @@ public:
static MessagePtrMut makeSubgiftMessage(const QString &text,
const QVariantMap &tags,
const QTime &time);
const QTime &time,
TwitchChannel *channel);
static MessagePtrMut makeMissingScopesMessage(const QString &missingScopes);
+3 -1
View File
@@ -667,6 +667,8 @@ void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message,
MessageSink &sink,
TwitchChannel *channel)
{
assert(channel != nullptr);
auto tags = message->tags();
auto parameters = message->parameters();
@@ -775,7 +777,7 @@ void IrcMessageHandler::parseUserNoticeMessageInto(Communi::IrcMessage *message,
// subgifts are special because they include two users
auto msg = MessageBuilder::makeSubgiftMessage(
parseTagString(messageText), tags,
calculateMessageTime(message).time());
calculateMessageTime(message).time(), channel);
msg->flags.set(MessageFlag::Subscription);
if (mirrored)
+48
View File
@@ -0,0 +1,48 @@
#include "providers/twitch/UserColor.hpp"
#include "common/ChannelChatters.hpp"
#include "controllers/userdata/UserDataController.hpp"
#include "messages/MessageColor.hpp"
namespace chatterino::twitch {
std::optional<MessageColor> getUserColor(const GetUserColorParams &params)
{
if (params.userDataController != nullptr)
{
if (!params.userID.isEmpty())
{
if (const auto &oUser =
params.userDataController->getUser(params.userID))
{
const auto &user = *oUser;
if (user.color && user.color->isValid())
{
return {*user.color};
}
}
}
}
if (params.color.isValid())
{
return {params.color};
}
if (params.channelChatters != nullptr)
{
if (!params.userLogin.isEmpty())
{
if (auto color =
params.channelChatters->getUserColor(params.userLogin);
color.isValid())
{
return {color};
}
}
}
return std::nullopt;
}
} // namespace chatterino::twitch
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "messages/MessageColor.hpp"
#include <QString>
#include <optional>
class QColor;
namespace chatterino {
class IUserDataController;
class ChannelChatters;
namespace twitch {
struct GetUserColorParams {
QString userLogin;
QString userID;
const IUserDataController *userDataController{};
const ChannelChatters *channelChatters{};
QColor color;
};
/// Attempt to get the color of the user based on the provided parameters.
///
/// If there's a valid color for the given user ID in the user data controller, return it.
/// If there's a valid `color` parameter, return it.
/// If there's a valid color for the given user login in the channel chatters set, return it.
/// Otherwise, return nullopt.
std::optional<MessageColor> getUserColor(const GetUserColorParams &params);
} // namespace twitch
} // namespace chatterino
+3
View File
@@ -104,6 +104,9 @@ const QStringList &getSampleSubMessages()
// resub without message
R"(@badges=subscriber/12;color=#CC00C2;display-name=cspice;emotes=;id=6fc4c3e0-ca61-454a-84b8-5669dee69fc9;login=cspice;mod=0;msg-id=resub;msg-param-months=12;msg-param-sub-plan-name=Channel\sSubscription\s(forsenlol):\s$9.99\sSub;msg-param-sub-plan=2000;room-id=22484632;subscriber=1;system-msg=cspice\sjust\ssubscribed\swith\sa\sTier\s2\ssub.\scspice\ssubscribed\sfor\s12\smonths\sin\sa\srow!;tmi-sent-ts=1528192510808;turbo=0;user-id=47894662;user-type= :tmi.twitch.tv USERNOTICE #pajlada)",
// pajlada gifted a sub to rustafur
R"(@tmi-sent-ts=1751793597378;subscriber=1;id=ac51c358-0525-468f-9e0f-e9f2b7953c29;room-id=11148817;user-id=11148817;login=pajlada;display-name=pajlada;badges=broadcaster/1,subscriber/3072,partner/1;badge-info=subscriber/114;color=#CC44FF;flags=;user-type=;emotes=;msg-param-sub-plan-name=look\sat\sthose\sshitty\semotes,\srip\s$5\sLUL;msg-param-gift-months=1;system-msg=pajlada\sgifted\sa\sTier\s1\ssub\sto\srustafur!;msg-param-months=6;msg-param-origin-id=4645652708379472175;msg-param-recipient-id=27787997;msg-param-sub-plan=1000;msg-id=subgift;msg-param-recipient-display-name=rustafur;msg-param-recipient-user-name=rustafur;msg-param-community-gift-id=4645652708379472175;msg-param-sender-count=0 :tmi.twitch.tv USERNOTICE #pajlada)",
};
return list;
}
+1
View File
@@ -57,6 +57,7 @@ set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/NativeMessaging.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ImageUploader.cpp
${CMAKE_CURRENT_LIST_DIR}/src/TwitchChannel.cpp
${CMAKE_CURRENT_LIST_DIR}/src/TwitchUserColor.cpp
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
@@ -0,0 +1,131 @@
{
"input": "@tmi-sent-ts=1751793597378;subscriber=1;id=ac51c358-0525-468f-9e0f-e9f2b7953c29;room-id=11148817;user-id=11148817;login=pajlada;display-name=pajlada;badges=broadcaster/1,subscriber/3072,partner/1;badge-info=subscriber/114;color=#CC44FF;flags=;user-type=;emotes=;msg-param-sub-plan-name=look\\sat\\sthose\\sshitty\\semotes,\\srip\\s$5\\sLUL;msg-param-gift-months=1;system-msg=pajlada\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\srustafur!;msg-param-months=6;msg-param-origin-id=4645652708379472175;msg-param-recipient-id=27787997;msg-param-sub-plan=1000;msg-id=subgift;msg-param-recipient-display-name=rustafur;msg-param-recipient-user-name=rustafur;msg-param-community-gift-id=4645652708379472175;msg-param-sender-count=0 :tmi.twitch.tv USERNOTICE #pajlada",
"output": [
{
"badgeInfos": {
},
"badges": [
],
"channelName": "",
"count": 1,
"displayName": "",
"elements": [
{
"element": {
"color": "System",
"flags": "Timestamp",
"link": {
"type": "None",
"value": ""
},
"style": "TimestampMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"9:19"
]
},
"flags": "Timestamp",
"format": "",
"link": {
"type": "None",
"value": ""
},
"time": "09:19:57",
"tooltip": "",
"trailingSpace": true,
"type": "TimestampElement"
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "MentionElement",
"userColor": "#ffcc44ff",
"userLoginName": "pajlada",
"words": [
"pajlada"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"gifted",
"a",
"Tier",
"1",
"sub",
"to"
]
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": false,
"type": "MentionElement",
"userColor": "#ffff00ff",
"userLoginName": "rustafur",
"words": [
"rustafur"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"!"
]
}
],
"flags": "System|DoNotTriggerNotification|Subscription",
"id": "",
"localizedName": "",
"loginName": "",
"messageText": "pajlada gifted a Tier 1 sub to rustafur!",
"searchText": "pajlada gifted a Tier 1 sub to rustafur!",
"serverReceivedTime": "",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
],
"params": {
"userData": {
"27787997": {
"color": "#FF00FF"
}
}
}
}
+11
View File
@@ -576,6 +576,17 @@ TEST_P(TestIrcMessageHandlerP, Run)
VectorMessageSink sink;
const auto &userData = snapshot->param("userData").toObject();
for (auto it = userData.begin(); it != userData.end(); ++it)
{
const auto &userID = it.key();
const auto &data = it.value().toObject();
if (auto color = data.value("color").toString(); !color.isEmpty())
{
this->mockApplication->getUserData()->setUserColor(userID, color);
}
}
for (auto prevInput : snapshot->param("prevMessages").toArray())
{
auto *ircMessage = Communi::IrcMessage::fromData(
+141
View File
@@ -0,0 +1,141 @@
#include "common/ChannelChatters.hpp"
#include "mocks/BaseApplication.hpp"
#include "mocks/Channel.hpp"
#include "mocks/Logging.hpp"
#include "mocks/UserData.hpp"
#include "providers/twitch/UserColor.hpp"
#include "Test.hpp"
#include <QString>
namespace chatterino::twitch {
namespace {
class MockApplication : public mock::BaseApplication
{
public:
MockApplication() = default;
ILogging *getChatLogger() override
{
return &this->logging;
}
mock::EmptyLogging logging;
};
} // namespace
class TwitchUserColor : public ::testing::Test
{
protected:
void SetUp() override
{
this->app = std::make_unique<MockApplication>();
this->userDataController = std::make_unique<mock::UserDataController>();
this->channel = std::make_unique<mock::MockChannel>("test");
this->chatters = std::make_unique<ChannelChatters>(*this->channel);
}
void TearDown() override
{
this->chatters.reset();
this->channel.reset();
this->userDataController.reset();
this->app.reset();
}
std::unique_ptr<MockApplication> app;
std::unique_ptr<mock::UserDataController> userDataController;
std::unique_ptr<mock::MockChannel> channel;
std::unique_ptr<ChannelChatters> chatters;
};
TEST_F(TwitchUserColor, NoDataForUser)
{
ASSERT_EQ(getUserColor({
.userLogin = "pajlada",
.userID = "11148817",
.userDataController = userDataController.get(),
.channelChatters = chatters.get(),
.color = {},
}),
std::nullopt);
}
TEST_F(TwitchUserColor, ChannelChattersData)
{
// User exists in channel chatters.
// Their color should be fetched from there.
chatters->setUserColor("pajlada", QColor("#FF0000"));
ASSERT_EQ(getUserColor({
.userLogin = "pajlada",
.userID = "11148817",
.userDataController = userDataController.get(),
.channelChatters = chatters.get(),
.color = {},
}),
QColor("#FF0000"));
}
TEST_F(TwitchUserColor, ChannelChattersDataButBaseColor)
{
// User exists in channel chatters
// The base color is valid and should be used.
chatters->setUserColor("pajlada", QColor("#FF0000"));
ASSERT_EQ(getUserColor({
.userLogin = "pajlada",
.userID = "11148817",
.userDataController = userDataController.get(),
.channelChatters = chatters.get(),
.color = QColor("#00FF00"),
}),
QColor("#00FF00"));
}
TEST_F(TwitchUserColor, ChannelChattersDataButBaseColorInvalid)
{
// User exists in channel chatters
// A base color has been defined but it's invalid
// The channel chatter color should be used
ASSERT_EQ(0, chatters->colorsSize());
chatters->setUserColor("pajlada", QColor("#FF0000"));
ASSERT_EQ(getUserColor({
.userLogin = "pajlada",
.userID = "11148817",
.userDataController = userDataController.get(),
.channelChatters = chatters.get(),
.color = QColor("this is an invalid color"),
}),
QColor("#FF0000"));
}
TEST_F(TwitchUserColor, UCD)
{
// User exists in channel chatters
// A base color has been defined
// User exists in user data controller
// The user data controller color should be used
chatters->setUserColor("pajlada", QColor("#FF0000"));
userDataController->setUserColor("11148817", "#0000FF");
ASSERT_EQ(getUserColor({
.userLogin = "pajlada",
.userID = "11148817",
.userDataController = userDataController.get(),
.channelChatters = chatters.get(),
.color = QColor("#00FF00"),
}),
QColor("#0000FF"));
}
} // namespace chatterino::twitch