Added support for Twitch's Chat Replies (#3722)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel Sage
2022-07-31 06:45:25 -04:00
committed by GitHub
parent a280089693
commit 20c974fdab
53 changed files with 2022 additions and 310 deletions
+108 -2
View File
@@ -9,7 +9,6 @@
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchHelpers.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
@@ -19,6 +18,7 @@
#include <IrcMessage>
#include <memory>
#include <unordered_set>
namespace {
@@ -242,6 +242,87 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
false, message->isAction());
}
std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
Channel *channel, Communi::IrcMessage *message,
const std::vector<MessagePtr> &otherLoaded)
{
std::vector<MessagePtr> builtMessages;
auto command = message->command();
if (command == "PRIVMSG")
{
auto privMsg = static_cast<Communi::IrcPrivateMessage *>(message);
auto tc = dynamic_cast<TwitchChannel *>(channel);
if (!tc)
{
return this->parsePrivMessage(channel, privMsg);
}
MessageParseArgs args;
TwitchMessageBuilder builder(channel, message, args, privMsg->content(),
privMsg->isAction());
this->populateReply(tc, message, otherLoaded, builder);
if (!builder.isIgnored())
{
builtMessages.emplace_back(builder.build());
builder.triggerHighlights();
}
}
else if (command == "USERNOTICE")
{
return this->parseUserNoticeMessage(channel, message);
}
else if (command == "NOTICE")
{
return this->parseNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
return builtMessages;
}
void IrcMessageHandler::populateReply(
TwitchChannel *channel, Communi::IrcMessage *message,
const std::vector<MessagePtr> &otherLoaded, TwitchMessageBuilder &builder)
{
const auto &tags = message->tags();
if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end())
{
const QString replyID = it.value().toString();
auto threadIt = channel->threads_.find(replyID);
if (threadIt != channel->threads_.end())
{
const auto owned = threadIt->second.lock();
if (owned)
{
// Thread already exists (has a reply)
builder.setThread(owned);
return;
}
}
// Thread does not yet exist, find root reply and create thread.
// Linear search is justified by the infrequent use of replies
for (auto &otherMsg : otherLoaded)
{
if (otherMsg->id == replyID)
{
// Found root reply message
std::shared_ptr<MessageThread> newThread =
std::make_shared<MessageThread>(otherMsg);
builder.setThread(newThread);
// Store weak reference to thread in channel
channel->addReplyThread(newThread);
break;
}
}
}
}
void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
const QString &target,
const QString &content,
@@ -276,7 +357,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
auto channel = dynamic_cast<TwitchChannel *>(chan.get());
const auto &tags = _message->tags();
if (const auto &it = tags.find("custom-reward-id"); it != tags.end())
if (const auto it = tags.find("custom-reward-id"); it != tags.end())
{
const auto rewardId = it.value().toString();
if (!channel->isChannelPointRewardKnown(rewardId))
@@ -301,6 +382,31 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end())
{
const QString replyID = it.value().toString();
auto threadIt = channel->threads_.find(replyID);
if (threadIt != channel->threads_.end() && !threadIt->second.expired())
{
// Thread already exists (has a reply)
builder.setThread(threadIt->second.lock());
}
else
{
// Thread does not yet exist, find root reply and create thread.
auto root = channel->findMessage(replyID);
if (root)
{
// Found root reply message
const auto newThread = std::make_shared<MessageThread>(root);
builder.setThread(newThread);
// Store weak reference to thread in channel
channel->addReplyThread(newThread);
}
}
}
if (isSub || !builder.isIgnored())
{
if (isSub)
@@ -3,6 +3,10 @@
#include <IrcMessage>
#include "common/Channel.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include <vector>
namespace chatterino {
@@ -20,6 +24,10 @@ public:
std::vector<MessagePtr> parseMessage(Channel *channel,
Communi::IrcMessage *message);
std::vector<MessagePtr> parseMessageWithReply(
Channel *channel, Communi::IrcMessage *message,
const std::vector<MessagePtr> &otherLoaded);
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
std::vector<MessagePtr> parsePrivMessage(
Channel *channel, Communi::IrcPrivateMessage *message);
@@ -59,6 +67,10 @@ private:
void addMessage(Communi::IrcMessage *message, const QString &target,
const QString &content, TwitchIrcServer &server,
bool isResub, bool isAction);
void populateReply(TwitchChannel *channel, Communi::IrcMessage *message,
const std::vector<MessagePtr> &otherLoaded,
TwitchMessageBuilder &builder);
};
} // namespace chatterino
+146 -42
View File
@@ -1,6 +1,5 @@
#include "providers/twitch/TwitchChannel.hpp"
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp"
@@ -182,12 +181,34 @@ TwitchChannel::TwitchChannel(const QString &name)
this->refreshBTTVChannelEmotes(false);
});
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
if (msg->replyThread)
{
if (msg->replyThread->liveCount(msg) == 0)
{
this->threads_.erase(msg->replyThread->rootId());
}
}
});
// timers
QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [=] {
this->refreshChatters();
});
this->chattersListTimer_.start(5 * 60 * 1000);
QObject::connect(&this->threadClearTimer_, &QTimer::timeout, [=] {
// We periodically check for any dangling reply threads that missed
// being cleaned up on messageRemovedFromStart. This could occur if
// some other part of the program, like a user card, held a reference
// to the message.
//
// It seems difficult to actually replicate a situation where things
// are actually cleaned up, but I've verified that cleanups DO happen.
this->cleanUpReplyThreads();
});
this->threadClearTimer_.start(5 * 60 * 1000);
// debugging
#if 0
for (int i = 0; i < 1000; i++) {
@@ -311,46 +332,34 @@ boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
return it->second;
}
void TwitchChannel::sendMessage(const QString &message)
void TwitchChannel::showLoginMessage()
{
const auto linkColor = MessageColor(MessageColor::Link);
const auto accountsLink = Link(Link::OpenAccountsPage, QString());
const auto currentUser = getApp()->accounts->twitch.getCurrent();
const auto expirationText =
QStringLiteral("You need to log in to send messages. You can link your "
"Twitch account");
const auto loginPromptText = QStringLiteral("in the settings.");
auto builder = MessageBuilder();
builder.message().flags.set(MessageFlag::System);
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
builder.emplace<TimestampElement>();
builder.emplace<TextElement>(expirationText, MessageElementFlag::Text,
MessageColor::System);
builder
.emplace<TextElement>(loginPromptText, MessageElementFlag::Text,
linkColor)
->setLink(accountsLink);
this->addMessage(builder.release());
}
QString TwitchChannel::prepareMessage(const QString &message) const
{
auto app = getApp();
if (!app->accounts->twitch.isLoggedIn())
{
if (message.isEmpty())
{
return;
}
const auto linkColor = MessageColor(MessageColor::Link);
const auto accountsLink = Link(Link::OpenAccountsPage, QString());
const auto currentUser = getApp()->accounts->twitch.getCurrent();
const auto expirationText =
QString("You need to log in to send messages. You can link your "
"Twitch account");
const auto loginPromptText = QString("in the settings.");
auto builder = MessageBuilder();
builder.message().flags.set(MessageFlag::System);
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
builder.emplace<TimestampElement>();
builder.emplace<TextElement>(expirationText, MessageElementFlag::Text,
MessageColor::System);
builder
.emplace<TextElement>(loginPromptText, MessageElementFlag::Text,
linkColor)
->setLink(accountsLink);
this->addMessage(builder.release());
return;
}
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Send message:" << message;
// Do last message processing
QString parsedMessage = app->emotes->emojis.replaceShortCodes(message);
// This is to make sure that combined emoji go through properly, see
@@ -361,7 +370,7 @@ void TwitchChannel::sendMessage(const QString &message)
if (parsedMessage.isEmpty())
{
return;
return "";
}
if (!this->hasHighRateLimit())
@@ -395,6 +404,32 @@ void TwitchChannel::sendMessage(const QString &message)
}
}
return parsedMessage;
}
void TwitchChannel::sendMessage(const QString &message)
{
auto app = getApp();
if (!app->accounts->twitch.isLoggedIn())
{
if (!message.isEmpty())
{
this->showLoginMessage();
}
return;
}
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Send message:" << message;
// Do last message processing
QString parsedMessage = this->prepareMessage(message);
if (parsedMessage.isEmpty())
{
return;
}
bool messageSent = false;
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
@@ -405,6 +440,40 @@ void TwitchChannel::sendMessage(const QString &message)
}
}
void TwitchChannel::sendReply(const QString &message, const QString &replyId)
{
auto app = getApp();
if (!app->accounts->twitch.isLoggedIn())
{
if (!message.isEmpty())
{
this->showLoginMessage();
}
return;
}
qCDebug(chatterinoTwitch) << "[TwitchChannel" << this->getName()
<< "] Send reply message:" << message;
// Do last message processing
QString parsedMessage = this->prepareMessage(message);
if (parsedMessage.isEmpty())
{
return;
}
bool messageSent = false;
this->sendReplySignal.invoke(this->getName(), parsedMessage, replyId,
messageSent);
if (messageSent)
{
qCDebug(chatterinoTwitch) << "sent";
this->lastSentMessage_ = parsedMessage;
}
}
bool TwitchChannel::isMod() const
{
return this->mod_;
@@ -794,8 +863,10 @@ void TwitchChannel::loadRecentMessages()
}
}
for (auto builtMessage :
handler.parseMessage(shared.get(), message))
auto builtMessages = handler.parseMessageWithReply(
shared.get(), message, allBuiltMessages);
for (auto builtMessage : builtMessages)
{
builtMessage->flags.set(MessageFlag::RecentMessage);
allBuiltMessages.emplace_back(builtMessage);
@@ -922,6 +993,39 @@ void TwitchChannel::fetchDisplayName()
[] {});
}
void TwitchChannel::addReplyThread(const std::shared_ptr<MessageThread> &thread)
{
this->threads_[thread->rootId()] = thread;
}
const std::unordered_map<QString, std::weak_ptr<MessageThread>>
&TwitchChannel::threads() const
{
return this->threads_;
}
void TwitchChannel::cleanUpReplyThreads()
{
for (auto it = this->threads_.begin(), last = this->threads_.end();
it != last;)
{
bool doErase = true;
if (auto thread = it->second.lock())
{
doErase = thread->liveCount() == 0;
}
if (doErase)
{
it = this->threads_.erase(it);
}
else
{
++it;
}
}
}
void TwitchChannel::refreshBadges()
{
auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" +
+22 -3
View File
@@ -1,5 +1,6 @@
#pragma once
#include "Application.hpp"
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include "common/Channel.hpp"
@@ -7,6 +8,7 @@
#include "common/ChatterSet.hpp"
#include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp"
#include "messages/MessageThread.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
@@ -74,12 +76,15 @@ public:
int slowMode = 0;
};
explicit TwitchChannel(const QString &channelName);
void initialize();
// Channel methods
virtual bool isEmpty() const override;
virtual bool canSendMessage() const override;
virtual void sendMessage(const QString &message) override;
virtual void sendReply(const QString &message, const QString &replyId);
virtual bool isMod() const override;
bool isVip() const;
bool isStaff() const;
@@ -118,6 +123,17 @@ public:
// Cheers
boost::optional<CheerEmote> cheerEmote(const QString &string);
// Replies
/**
* Stores the given thread in this channel.
*
* Note: This method not take ownership of the MessageThread; this
* TwitchChannel instance will store a weak_ptr to the thread.
*/
void addReplyThread(const std::shared_ptr<MessageThread> &thread);
const std::unordered_map<QString, std::weak_ptr<MessageThread>> &threads()
const;
// Signals
pajlada::Signals::NoArgSignal roomIdChanged;
pajlada::Signals::NoArgSignal userStateChanged;
@@ -138,9 +154,6 @@ private:
QString localizedName;
} nameOptions;
protected:
explicit TwitchChannel(const QString &channelName);
private:
// Methods
void refreshLiveStatus();
@@ -151,6 +164,8 @@ private:
void refreshCheerEmotes();
void loadRecentMessages();
void fetchDisplayName();
void cleanUpReplyThreads();
void showLoginMessage();
void setLive(bool newLiveStatus);
void setMod(bool value);
@@ -164,6 +179,8 @@ private:
const QString &getDisplayName() const override;
const QString &getLocalizedName() const override;
QString prepareMessage(const QString &message) const;
// Data
const QString subscriptionUrl_;
const QString channelUrl_;
@@ -171,6 +188,7 @@ private:
int chatterCount_;
UniqueAccess<StreamStatus> streamStatus_;
UniqueAccess<RoomModes> roomModes_;
std::unordered_map<QString, std::weak_ptr<MessageThread>> threads_;
protected:
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
@@ -194,6 +212,7 @@ private:
QString lastSentMessage_;
QObject lifetimeGuard_;
QTimer chattersListTimer_;
QTimer threadClearTimer_;
QElapsedTimer titleRefreshedTimer_;
QElapsedTimer clipCreationTimer_;
bool isClipCreationInProgress{false};
+77 -48
View File
@@ -121,6 +121,11 @@ std::shared_ptr<Channel> TwitchIrcServer::createChannel(
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
this->onMessageSendRequested(channel, msg, sent);
});
channel->sendReplySignal.connect(
[this, channel = channel.get()](auto &chan, auto &msg, auto &replyId,
bool &sent) {
this->onReplySendRequested(channel, msg, replyId, sent);
});
return std::shared_ptr<Channel>(channel);
}
@@ -370,66 +375,90 @@ bool TwitchIrcServer::hasSeparateWriteConnection() const
// return getSettings()->twitchSeperateWriteConnection;
}
bool TwitchIrcServer::prepareToSend(TwitchChannel *channel)
{
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
auto &lastMessage = channel->hasHighRateLimit() ? this->lastMessageMod_
: this->lastMessagePleb_;
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
auto now = std::chrono::steady_clock::now();
// check if you are sending messages too fast
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
{
if (this->lastErrorTimeSpeed_ + 30s < now)
{
auto errorMessage =
makeSystemMessage("You are sending messages too quickly.");
channel->addMessage(errorMessage);
this->lastErrorTimeSpeed_ = now;
}
return false;
}
// remove messages older than 30 seconds
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
{
lastMessage.pop();
}
// check if you are sending too many messages
if (lastMessage.size() >= maxMessageCount)
{
if (this->lastErrorTimeAmount_ + 30s < now)
{
auto errorMessage =
makeSystemMessage("You are sending too many messages.");
channel->addMessage(errorMessage);
this->lastErrorTimeAmount_ = now;
}
return false;
}
lastMessage.push(now);
return true;
}
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
const QString &message, bool &sent)
{
sent = false;
bool canSend = this->prepareToSend(channel);
if (!canSend)
{
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
// std::queue<std::chrono::steady_clock::time_point>
auto &lastMessage = channel->hasHighRateLimit()
? this->lastMessageMod_
: this->lastMessagePleb_;
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
auto now = std::chrono::steady_clock::now();
// check if you are sending messages too fast
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
{
if (this->lastErrorTimeSpeed_ + 30s < now)
{
auto errorMessage =
makeSystemMessage("You are sending messages too quickly.");
channel->addMessage(errorMessage);
this->lastErrorTimeSpeed_ = now;
}
return;
}
// remove messages older than 30 seconds
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
{
lastMessage.pop();
}
// check if you are sending too many messages
if (lastMessage.size() >= maxMessageCount)
{
if (this->lastErrorTimeAmount_ + 30s < now)
{
auto errorMessage =
makeSystemMessage("You are sending too many messages.");
channel->addMessage(errorMessage);
this->lastErrorTimeAmount_ = now;
}
return;
}
lastMessage.push(now);
return;
}
this->sendMessage(channel->getName(), message);
sent = true;
}
void TwitchIrcServer::onReplySendRequested(TwitchChannel *channel,
const QString &message,
const QString &replyId, bool &sent)
{
sent = false;
bool canSend = this->prepareToSend(channel);
if (!canSend)
{
return;
}
this->sendRawMessage("@reply-parent-msg-id=" + replyId + " PRIVMSG #" +
channel->getName() + " :" + message);
sent = true;
}
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
{
return this->bttv;
+4
View File
@@ -67,6 +67,10 @@ protected:
private:
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
bool &sent);
void onReplySendRequested(TwitchChannel *channel, const QString &message,
const QString &replyId, bool &sent);
bool prepareToSend(TwitchChannel *channel);
std::mutex lastMessageMutex_;
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
+147 -47
View File
@@ -48,6 +48,66 @@ const QSet<QString> zeroWidthEmotes{
namespace chatterino {
namespace {
QString stylizeUsername(const QString &username, const Message &message)
{
auto app = getApp();
const QString &localizedName = message.localizedName;
bool hasLocalizedName = !localizedName.isEmpty();
// The full string that will be rendered in the chat widget
QString usernameText;
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
usernameText = username;
}
break;
case UsernameDisplayMode::LocalizedName: {
if (hasLocalizedName)
{
usernameText = localizedName;
}
else
{
usernameText = username;
}
}
break;
default:
case UsernameDisplayMode::UsernameAndLocalizedName: {
if (hasLocalizedName)
{
usernameText = username + "(" + localizedName + ")";
}
else
{
usernameText = username;
}
}
break;
}
auto nicknames = getCSettings().nicknames.readOnly();
for (const auto &nickname : *nicknames)
{
if (nickname.match(usernameText))
{
break;
}
}
return usernameText;
}
} // namespace
TwitchMessageBuilder::TwitchMessageBuilder(
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args)
@@ -138,6 +198,70 @@ MessagePtr TwitchMessageBuilder::build()
this->message().flags.set(MessageFlag::FirstMessage);
}
// reply threads
if (this->thread_)
{
// set references
this->message().replyThread = this->thread_;
this->thread_->addToThread(this->weakOf());
// enable reply flag
this->message().flags.set(MessageFlag::ReplyMessage);
const auto &threadRoot = this->thread_->root();
QString usernameText =
stylizeUsername(threadRoot->loginName, *threadRoot.get());
this->emplace<ReplyCurveElement>();
// construct reply elements
this->emplace<TextElement>(
"Replying to", MessageElementFlag::RepliedMessage,
MessageColor::System, FontStyle::ChatMediumSmall)
->setLink({Link::ViewThread, this->thread_->rootId()});
this->emplace<TextElement>(
"@" + usernameText + ":", MessageElementFlag::RepliedMessage,
threadRoot->usernameColor, FontStyle::ChatMediumSmall)
->setLink({Link::UserInfo, threadRoot->displayName});
this->emplace<SingleLineTextElement>(
threadRoot->messageText, MessageElementFlag::RepliedMessage,
this->textColor_, FontStyle::ChatMediumSmall)
->setLink({Link::ViewThread, this->thread_->rootId()});
}
else if (this->tags.find("reply-parent-msg-id") != this->tags.end())
{
// Message is a reply but we couldn't find the original message.
// Render the message using the additional reply tags
auto replyDisplayName = this->tags.find("reply-parent-display-name");
auto replyBody = this->tags.find("reply-parent-msg-body");
if (replyDisplayName != this->tags.end() &&
replyBody != this->tags.end())
{
auto name = replyDisplayName->toString();
auto body = parseTagString(replyBody->toString());
this->emplace<ReplyCurveElement>();
this->emplace<TextElement>(
"Replying to", MessageElementFlag::RepliedMessage,
MessageColor::System, FontStyle::ChatMediumSmall);
this->emplace<TextElement>(
"@" + name + ":", MessageElementFlag::RepliedMessage,
this->textColor_, FontStyle::ChatMediumSmall)
->setLink({Link::UserInfo, name});
this->emplace<SingleLineTextElement>(
body, MessageElementFlag::RepliedMessage, this->textColor_,
FontStyle::ChatMediumSmall);
}
}
// timestamp
this->message().serverReceivedTime = calculateMessageTime(this->ircMessage);
this->emplace<TimestampElement>(this->message().serverReceivedTime.time());
@@ -217,6 +341,23 @@ MessagePtr TwitchMessageBuilder::build()
ColorProvider::instance().color(ColorType::Whisper);
}
if (this->thread_)
{
auto &img = getResources().buttons.replyThreadDark;
this->emplace<CircularImageElement>(Image::fromPixmap(img, 0.15), 2,
Qt::gray,
MessageElementFlag::ReplyButton)
->setLink({Link::ViewThread, this->thread_->rootId()});
}
else
{
auto &img = getResources().buttons.replyDark;
this->emplace<CircularImageElement>(Image::fromPixmap(img, 0.15), 2,
Qt::gray,
MessageElementFlag::ReplyButton)
->setLink({Link::ReplyToMessage, this->message().id});
}
return this->release();
}
@@ -559,53 +700,7 @@ void TwitchMessageBuilder::appendUsername()
}
}
bool hasLocalizedName = !localizedName.isEmpty();
// The full string that will be rendered in the chat widget
QString usernameText;
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
usernameText = username;
}
break;
case UsernameDisplayMode::LocalizedName: {
if (hasLocalizedName)
{
usernameText = localizedName;
}
else
{
usernameText = username;
}
}
break;
default:
case UsernameDisplayMode::UsernameAndLocalizedName: {
if (hasLocalizedName)
{
usernameText = username + "(" + localizedName + ")";
}
else
{
usernameText = username;
}
}
break;
}
auto nicknames = getCSettings().nicknames.readOnly();
for (const auto &nickname : *nicknames)
{
if (nickname.match(usernameText))
{
break;
}
}
QString usernameText = stylizeUsername(username, this->message());
if (this->args.isSentWhisper)
{
@@ -1479,4 +1574,9 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix,
}
}
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
{
this->thread_ = std::move(thread);
}
} // namespace chatterino
@@ -2,6 +2,7 @@
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "messages/MessageThread.hpp"
#include "messages/SharedMessageBuilder.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/PubSubActions.hpp"
@@ -45,6 +46,8 @@ public:
void triggerHighlights() override;
MessagePtr build() override;
void setThread(std::shared_ptr<MessageThread> thread);
static void appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
bool isBroadcaster);
@@ -105,6 +108,7 @@ private:
int bitsLeft;
bool bitsStacked = false;
bool historicalMessage_ = false;
std::shared_ptr<MessageThread> thread_;
QString userId_;
bool senderIsBroadcaster{};