refactor: Remove Leading Mention in Replies and Highlight Participated Threads (#4047)

This commit is contained in:
nerix 2022-10-08 16:25:32 +02:00 committed by GitHub
parent 29272e130a
commit 4e2da540d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 286 additions and 20 deletions

View file

@ -2,7 +2,7 @@
## Unversioned
- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041)
- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041, #4047)
- Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694, #3875)
- Minor: Added highlights for `Elevated Messages`. (#4016)
- Minor: Removed total views from the usercard, as Twitch no longer updates the number. (#3792)

View file

@ -11,10 +11,12 @@ auto highlightPhraseCheck(const HighlightPhrase &highlight) -> HighlightCheck
return HighlightCheck{
[highlight](const auto &args, const auto &badges,
const auto &senderName, const auto &originalMessage,
const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
(void)args; // unused
(void)badges; // unused
(void)senderName; // unused
(void)flags; // unused
if (self)
{
@ -60,11 +62,12 @@ void rebuildSubscriptionHighlights(Settings &settings,
checks.emplace_back(HighlightCheck{
[=](const auto &args, const auto &badges, const auto &senderName,
const auto &originalMessage,
const auto &originalMessage, const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
(void)badges; // unused
(void)senderName; // unused
(void)originalMessage; // unused
(void)flags; // unused
(void)self; // unused
if (!args.isSubscriptionMessage)
@ -105,11 +108,12 @@ void rebuildWhisperHighlights(Settings &settings,
checks.emplace_back(HighlightCheck{
[=](const auto &args, const auto &badges, const auto &senderName,
const auto &originalMessage,
const auto &originalMessage, const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
(void)badges; // unused
(void)senderName; // unused
(void)originalMessage; // unused
(void)flags; // unused
(void)self; // unused
if (!args.isReceivedWhisper)
@ -128,6 +132,44 @@ void rebuildWhisperHighlights(Settings &settings,
}
}
void rebuildReplyThreadHighlight(Settings &settings,
std::vector<HighlightCheck> &checks)
{
if (settings.enableThreadHighlight)
{
auto highlightSound = settings.enableThreadHighlightSound.getValue();
auto highlightAlert = settings.enableThreadHighlightTaskbar.getValue();
auto highlightSoundUrlValue =
settings.threadHighlightSoundUrl.getValue();
boost::optional<QUrl> highlightSoundUrl;
if (!highlightSoundUrlValue.isEmpty())
{
highlightSoundUrl = highlightSoundUrlValue;
}
auto highlightInMentions =
settings.showThreadHighlightInMentions.getValue();
checks.emplace_back(HighlightCheck{
[=](const auto & /*args*/, const auto & /*badges*/,
const auto & /*senderName*/, const auto & /*originalMessage*/,
const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
if (flags.has(MessageFlag::ParticipatedThread) && !self)
{
return HighlightResult{
highlightAlert,
highlightSound,
highlightSoundUrl,
ColorProvider::instance().color(
ColorType::ThreadMessageHighlight),
highlightInMentions,
};
}
return boost::none;
}});
}
}
void rebuildMessageHighlights(Settings &settings,
std::vector<HighlightCheck> &checks)
{
@ -163,10 +205,12 @@ void rebuildUserHighlights(Settings &settings,
checks.emplace_back(HighlightCheck{
[highlight](const auto &args, const auto &badges,
const auto &senderName, const auto &originalMessage,
const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
(void)args; // unused
(void)badges; // unused
(void)originalMessage; // unused
(void)flags; // unused
(void)self; // unused
if (!highlight.isMatch(senderName))
@ -201,10 +245,12 @@ void rebuildBadgeHighlights(Settings &settings,
checks.emplace_back(HighlightCheck{
[highlight](const auto &args, const auto &badges,
const auto &senderName, const auto &originalMessage,
const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
(void)args; // unused
(void)senderName; // unused
(void)originalMessage; // unused
(void)flags; // unused
(void)self; // unused
for (const Badge &badge : badges)
@ -247,6 +293,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
this->rebuildListener_.addSetting(settings.enableSubHighlight);
this->rebuildListener_.addSetting(settings.enableSubHighlightSound);
this->rebuildListener_.addSetting(settings.enableSubHighlightTaskbar);
this->rebuildListener_.addSetting(settings.enableThreadHighlight);
this->rebuildListener_.addSetting(settings.enableThreadHighlightSound);
this->rebuildListener_.addSetting(settings.enableThreadHighlightTaskbar);
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
this->rebuildListener_.setCB([this, &settings] {
qCDebug(chatterinoHighlights)
@ -294,7 +345,7 @@ void HighlightController::rebuildChecks(Settings &settings)
checks->clear();
// CURRENT ORDER:
// Subscription -> Whisper -> User -> Message -> Badge
// Subscription -> Whisper -> User -> Message -> Reply Threads -> Badge
rebuildSubscriptionHighlights(settings, *checks);
@ -304,12 +355,15 @@ void HighlightController::rebuildChecks(Settings &settings)
rebuildMessageHighlights(settings, *checks);
rebuildReplyThreadHighlight(settings, *checks);
rebuildBadgeHighlights(settings, *checks);
}
std::pair<bool, HighlightResult> HighlightController::check(
const MessageParseArgs &args, const std::vector<Badge> &badges,
const QString &senderName, const QString &originalMessage) const
const QString &senderName, const QString &originalMessage,
const MessageFlags &messageFlags) const
{
bool highlighted = false;
auto result = HighlightResult::emptyResult();
@ -322,8 +376,8 @@ std::pair<bool, HighlightResult> HighlightController::check(
for (const auto &check : *checks)
{
if (auto checkResult =
check.cb(args, badges, senderName, originalMessage, self);
if (auto checkResult = check.cb(args, badges, senderName,
originalMessage, messageFlags, self);
checkResult)
{
highlighted = true;

View file

@ -142,7 +142,8 @@ struct HighlightResult {
struct HighlightCheck {
using Checker = std::function<boost::optional<HighlightResult>(
const MessageParseArgs &args, const std::vector<Badge> &badges,
const QString &senderName, const QString &originalMessage, bool self)>;
const QString &senderName, const QString &originalMessage,
const MessageFlags &messageFlags, bool self)>;
Checker cb;
};
@ -156,7 +157,8 @@ public:
**/
[[nodiscard]] std::pair<bool, HighlightResult> check(
const MessageParseArgs &args, const std::vector<Badge> &badges,
const QString &senderName, const QString &originalMessage) const;
const QString &senderName, const QString &originalMessage,
const MessageFlags &messageFlags) const;
private:
/**

View file

@ -213,6 +213,36 @@ void HighlightModel::afterInit()
this->insertCustomRow(elevatedMessageRow,
HighlightRowIndexes::ElevatedMessageRow);
// Highlight settings for reply threads
std::vector<QStandardItem *> threadMessageRow = this->createRow();
setBoolItem(threadMessageRow[Column::Pattern],
getSettings()->enableThreadHighlight.getValue(), true, false);
threadMessageRow[Column::Pattern]->setData("Participated Reply Threads",
Qt::DisplayRole);
setBoolItem(threadMessageRow[Column::ShowInMentions],
getSettings()->showThreadHighlightInMentions.getValue(), true,
false);
setBoolItem(threadMessageRow[Column::FlashTaskbar],
getSettings()->enableThreadHighlightTaskbar.getValue(), true,
false);
setBoolItem(threadMessageRow[Column::PlaySound],
getSettings()->enableThreadHighlightSound.getValue(), true,
false);
threadMessageRow[Column::UseRegex]->setFlags({});
threadMessageRow[Column::CaseSensitive]->setFlags({});
QUrl threadMessageSound =
QUrl(getSettings()->threadHighlightSoundUrl.getValue());
setFilePathItem(threadMessageRow[Column::SoundPath], threadMessageSound,
false);
auto threadMessageColor =
ColorProvider::instance().color(ColorType::ThreadMessageHighlight);
setColorItem(threadMessageRow[Column::Color], *threadMessageColor, false);
this->insertCustomRow(threadMessageRow,
HighlightRowIndexes::ThreadMessageRow);
}
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
@ -252,6 +282,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableElevatedMessageHighlight.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->enableThreadHighlight.setValue(
value.toBool());
}
}
}
break;
@ -263,6 +298,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->showSelfHighlightInMentions.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->showThreadHighlightInMentions.setValue(
value.toBool());
}
}
}
break;
@ -300,6 +340,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
// ->enableElevatedMessageHighlightTaskbar.setvalue(
// value.toBool());
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->enableThreadHighlightTaskbar.setValue(
value.toBool());
}
}
}
break;
@ -336,6 +381,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
// getSettings()->enableElevatedMessageHighlightSound.setValue(
// value.toBool());
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->enableThreadHighlightSound.setValue(
value.toBool());
}
}
}
break;
@ -381,6 +431,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->elevatedMessageHighlightSoundUrl.setValue(
value.toString());
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->threadHighlightSoundUrl.setValue(
value.toString());
}
}
}
break;
@ -424,6 +479,13 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
.updateColor(ColorType::ElevatedMessageHighlight,
QColor(colorName));
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->threadHighlightColor.setValue(colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::ThreadMessageHighlight,
QColor(colorName));
}
}
}
break;

View file

@ -32,6 +32,7 @@ public:
RedeemedRow = 3,
FirstMessageRow = 4,
ElevatedMessageRow = 5,
ThreadMessageRow = 6,
};
protected:

View file

@ -16,6 +16,8 @@ QColor HighlightPhrase::FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR =
QColor(72, 127, 63, 60);
QColor HighlightPhrase::FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR =
QColor(255, 174, 66, 60);
QColor HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR =
QColor(143, 48, 24, 60);
QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100);
bool HighlightPhrase::operator==(const HighlightPhrase &other) const

View file

@ -84,6 +84,7 @@ public:
static QColor FALLBACK_SUB_COLOR;
static QColor FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR;
static QColor FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR;
static QColor FALLBACK_THREAD_HIGHLIGHT_COLOR;
private:
QString pattern_;

View file

@ -43,6 +43,7 @@ enum class MessageFlag : int64_t {
FirstMessage = (1LL << 23),
ReplyMessage = (1LL << 24),
ElevatedMessage = (1LL << 25),
ParticipatedThread = (1LL << 26),
};
using MessageFlags = FlagsEnum<MessageFlag>;

View file

@ -58,4 +58,14 @@ size_t MessageThread::liveCount(
return count;
}
bool MessageThread::participated() const
{
return this->participated_;
}
void MessageThread::markParticipated()
{
this->participated_ = true;
}
} // namespace chatterino

View file

@ -23,6 +23,10 @@ public:
/// Returns the number of live reply references
size_t liveCount(const std::shared_ptr<const Message> &exclude) const;
bool participated() const;
void markParticipated();
const QString &rootId() const
{
return rootMessageId_;
@ -42,6 +46,7 @@ private:
const QString rootMessageId_;
const std::shared_ptr<const Message> rootMessage_;
std::vector<std::weak_ptr<const Message>> replies_;
bool participated_ = false;
};
} // namespace chatterino

View file

@ -149,7 +149,8 @@ void SharedMessageBuilder::parseHighlights()
auto badges = SharedMessageBuilder::parseBadgeTag(this->tags);
auto [highlighted, highlightResult] = getIApp()->getHighlights()->check(
this->args, badges, this->ircMessage->nick(), this->originalMessage_);
this->args, badges, this->ircMessage->nick(), this->originalMessage_,
this->message().flags);
if (!highlighted)
{

View file

@ -147,6 +147,20 @@ void ColorProvider::initTypeColorMap()
std::make_shared<QColor>(
HighlightPhrase::FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR)});
}
customColor = getSettings()->threadHighlightColor;
if (QColor(customColor).isValid())
{
this->typeColorMap_.insert({ColorType::ThreadMessageHighlight,
std::make_shared<QColor>(customColor)});
}
else
{
this->typeColorMap_.insert(
{ColorType::ThreadMessageHighlight,
std::make_shared<QColor>(
HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR)});
}
}
void ColorProvider::initDefaultColors()

View file

@ -14,6 +14,7 @@ enum class ColorType {
RedeemedHighlight,
FirstMessageHighlight,
ElevatedMessageHighlight,
ThreadMessageHighlight,
};
class ColorProvider

View file

@ -67,6 +67,65 @@ MessagePtr generateBannedMessage(bool confirmedBan)
return builder.release();
}
int stripLeadingReplyMention(const QVariantMap &tags, QString &content)
{
if (!getSettings()->stripReplyMention)
{
return 0;
}
if (const auto it = tags.find("reply-parent-display-name");
it != tags.end())
{
auto displayName = it.value().toString();
if (content.startsWith('@') &&
content.at(1 + displayName.length()) == ' ' &&
content.indexOf(displayName, 1) == 1)
{
int messageOffset = 1 + displayName.length() + 1;
content.remove(0, messageOffset);
return messageOffset;
}
}
return 0;
}
void updateReplyParticipatedStatus(const QVariantMap &tags,
const QString &senderLogin,
TwitchMessageBuilder &builder,
std::shared_ptr<MessageThread> &thread,
bool isNew)
{
const auto &currentLogin =
getApp()->accounts->twitch.getCurrent()->getUserName();
if (thread->participated())
{
builder.message().flags.set(MessageFlag::ParticipatedThread);
return;
}
if (isNew)
{
if (const auto it = tags.find("reply-parent-user-login");
it != tags.end())
{
auto name = it.value().toString();
if (name == currentLogin)
{
thread->markParticipated();
builder.message().flags.set(MessageFlag::ParticipatedThread);
return; // already marked as participated
}
}
}
if (senderLogin == currentLogin)
{
thread->markParticipated();
// don't set the highlight here
}
}
} // namespace
namespace chatterino {
@ -259,9 +318,12 @@ std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
return this->parsePrivMessage(channel, privMsg);
}
QString content = privMsg->content();
int messageOffset = stripLeadingReplyMention(privMsg->tags(), content);
MessageParseArgs args;
TwitchMessageBuilder builder(channel, message, args, privMsg->content(),
TwitchMessageBuilder builder(channel, message, args, content,
privMsg->isAction());
builder.setMessageOffset(messageOffset);
this->populateReply(tc, message, otherLoaded, builder);
@ -295,10 +357,12 @@ void IrcMessageHandler::populateReply(
auto threadIt = channel->threads_.find(replyID);
if (threadIt != channel->threads_.end())
{
const auto owned = threadIt->second.lock();
auto owned = threadIt->second.lock();
if (owned)
{
// Thread already exists (has a reply)
updateReplyParticipatedStatus(tags, message->nick(), builder,
owned, false);
builder.setThread(owned);
return;
}
@ -331,6 +395,8 @@ void IrcMessageHandler::populateReply(
{
std::shared_ptr<MessageThread> newThread =
std::make_shared<MessageThread>(foundMessage);
updateReplyParticipatedStatus(tags, message->nick(), builder,
newThread, true);
builder.setThread(newThread);
// Store weak reference to thread in channel
@ -341,7 +407,7 @@ void IrcMessageHandler::populateReply(
void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
const QString &target,
const QString &content,
const QString &content_,
TwitchIrcServer &server, bool isSub,
bool isAction)
{
@ -384,7 +450,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
[=, &server](ChannelPointReward reward) {
if (reward.id == rewardId)
{
this->addMessage(clone, target, content, server, isSub,
this->addMessage(clone, target, content_, server, isSub,
isAction);
clone->deleteLater();
return true;
@ -396,7 +462,11 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
args.channelPointRewardId = rewardId;
}
QString content = content_;
int messageOffset = stripLeadingReplyMention(tags, content);
TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
builder.setMessageOffset(messageOffset);
if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end())
{
@ -405,7 +475,10 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
if (threadIt != channel->threads_.end() && !threadIt->second.expired())
{
// Thread already exists (has a reply)
builder.setThread(threadIt->second.lock());
auto thread = threadIt->second.lock();
updateReplyParticipatedStatus(tags, _message->nick(), builder,
thread, false);
builder.setThread(thread);
}
else
{
@ -414,7 +487,9 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
if (root)
{
// Found root reply message
const auto newThread = std::make_shared<MessageThread>(root);
auto newThread = std::make_shared<MessageThread>(root);
updateReplyParticipatedStatus(tags, _message->nick(), builder,
newThread, true);
builder.setThread(newThread);
// Store weak reference to thread in channel

View file

@ -999,8 +999,10 @@ void TwitchMessageBuilder::appendTwitchEmote(
return;
}
auto start = correctPositions[coords.at(0).toUInt()];
auto end = correctPositions[coords.at(1).toUInt()];
auto start =
correctPositions[coords.at(0).toUInt() - this->messageOffset_];
auto end =
correctPositions[coords.at(1).toUInt() - this->messageOffset_];
if (start >= end || start < 0 || end > this->originalMessage_.length())
{
@ -1589,4 +1591,9 @@ void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
this->thread_ = std::move(thread);
}
void TwitchMessageBuilder::setMessageOffset(int offset)
{
this->messageOffset_ = offset;
}
} // namespace chatterino

View file

@ -47,6 +47,7 @@ public:
MessagePtr build() override;
void setThread(std::shared_ptr<MessageThread> thread);
void setMessageOffset(int offset);
static void appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
@ -110,6 +111,18 @@ private:
bool historicalMessage_ = false;
std::shared_ptr<MessageThread> thread_;
/**
* Starting offset to be used on index-based operations on `originalMessage_`.
*
* For example:
* originalMessage_ = "there"
* messageOffset_ = 4
* (the irc message is "hey there")
*
* then the index 6 would resolve to 6 - 4 = 2 => 'e'
*/
int messageOffset_ = 0;
QString userId_;
bool senderIsBroadcaster{};
};

View file

@ -118,6 +118,7 @@ public:
// BoolSetting collapseLongMessages =
// {"/appearance/messages/collapseLongMessages", false};
BoolSetting showReplyButton = {"/appearance/showReplyButton", false};
BoolSetting stripReplyMention = {"/appearance/stripReplyMention", true};
IntSetting collpseMessagesMinLines = {
"/appearance/messages/collapseMessagesMinLines", 0};
BoolSetting alternateMessages = {
@ -327,6 +328,19 @@ public:
""};
QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""};
BoolSetting enableThreadHighlight = {
"/highlighting/thread/nameIsHighlightKeyword", true};
BoolSetting showThreadHighlightInMentions = {
"/highlighting/thread/showSelfHighlightInMentions", true};
BoolSetting enableThreadHighlightSound = {
"/highlighting/thread/enableSound", true};
BoolSetting enableThreadHighlightTaskbar = {
"/highlighting/thread/enableTaskbarFlashing", true};
QStringSetting threadHighlightSoundUrl = {
"/highlighting/threadHighlightSoundUrl", ""};
QStringSetting threadHighlightColor = {"/highlighting/threadHighlightColor",
""};
QStringSetting highlightColor = {"/highlighting/color", ""};
BoolSetting longAlerts = {"/highlighting/alerts", false};

View file

@ -719,6 +719,7 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Combine multiple bit tips into one", s.stackBits);
layout.addCheckbox("Messages in /mentions highlights tab",
s.highlightMentions);
layout.addCheckbox("Strip leading mention in replies", s.stripReplyMention);
// Helix timegate settings
auto helixTimegateGetValue = [](auto val) {

View file

@ -485,6 +485,7 @@ struct TestCase {
std::vector<Badge> badges;
QString senderName;
QString originalMessage;
MessageFlags flags;
} input;
struct {
@ -727,8 +728,9 @@ TEST_F(HighlightControllerTest, A)
for (const auto &[input, expected] : tests)
{
auto [isMatch, matchResult] = this->controller->check(
input.args, input.badges, input.senderName, input.originalMessage);
auto [isMatch, matchResult] =
this->controller->check(input.args, input.badges, input.senderName,
input.originalMessage, input.flags);
EXPECT_EQ(isMatch, expected.state)
<< qUtf8Printable(input.senderName) << ": "