From e4af009fda9f277d6eacaa3d29b6f5a557409359 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 4 Jul 2020 09:15:59 -0400 Subject: [PATCH] [irc] Partially fix IRC colors (#1594) Doesn't fix #1379 but it is a big step forward. Needs some "real life" testing, but should be good. --- chatterino.pro | 5 + src/common/IrcColors.hpp | 62 +++ src/messages/MessageBuilder.hpp | 2 +- src/messages/MessageElement.cpp | 205 +++++++++ src/messages/MessageElement.hpp | 30 ++ src/messages/SharedMessageBuilder.cpp | 399 ++++++++++++++++++ src/messages/SharedMessageBuilder.hpp | 69 +++ src/messages/layouts/MessageLayoutElement.cpp | 37 ++ src/messages/layouts/MessageLayoutElement.hpp | 22 +- src/providers/irc/IrcMessageBuilder.cpp | 93 ++++ src/providers/irc/IrcMessageBuilder.hpp | 41 ++ src/providers/irc/IrcServer.cpp | 28 +- src/providers/twitch/TwitchCommon.hpp | 19 + src/providers/twitch/TwitchIrcServer.cpp | 1 - src/providers/twitch/TwitchMessageBuilder.cpp | 386 ++--------------- src/providers/twitch/TwitchMessageBuilder.hpp | 37 +- 16 files changed, 1033 insertions(+), 403 deletions(-) create mode 100644 src/common/IrcColors.hpp create mode 100644 src/messages/SharedMessageBuilder.cpp create mode 100644 src/messages/SharedMessageBuilder.hpp create mode 100644 src/providers/irc/IrcMessageBuilder.cpp create mode 100644 src/providers/irc/IrcMessageBuilder.hpp diff --git a/chatterino.pro b/chatterino.pro index da773884..0be84066 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -156,6 +156,7 @@ SOURCES += \ src/messages/MessageColor.cpp \ src/messages/MessageContainer.cpp \ src/messages/MessageElement.cpp \ + src/messages/SharedMessageBuilder.cpp \ src/messages/search/AuthorPredicate.cpp \ src/messages/search/LinkPredicate.cpp \ src/messages/search/SubstringPredicate.cpp \ @@ -171,6 +172,7 @@ SOURCES += \ src/providers/irc/IrcChannel2.cpp \ src/providers/irc/IrcCommands.cpp \ src/providers/irc/IrcConnection2.cpp \ + src/providers/irc/IrcMessageBuilder.cpp \ src/providers/irc/IrcServer.cpp \ src/providers/LinkResolver.cpp \ src/providers/twitch/api/Helix.cpp \ @@ -298,6 +300,7 @@ HEADERS += \ src/common/DownloadManager.hpp \ src/common/Env.hpp \ src/common/FlagsEnum.hpp \ + src/common/IrcColors.hpp \ src/common/LinkParser.hpp \ src/common/Modes.hpp \ src/common/NetworkCommon.hpp \ @@ -353,6 +356,7 @@ HEADERS += \ src/messages/MessageContainer.hpp \ src/messages/MessageElement.hpp \ src/messages/MessageParseArgs.hpp \ + src/messages/SharedMessageBuilder.hpp \ src/messages/search/AuthorPredicate.hpp \ src/messages/search/LinkPredicate.hpp \ src/messages/search/MessagePredicate.hpp \ @@ -371,6 +375,7 @@ HEADERS += \ src/providers/irc/IrcChannel2.hpp \ src/providers/irc/IrcCommands.hpp \ src/providers/irc/IrcConnection2.hpp \ + src/providers/irc/IrcMessageBuilder.hpp \ src/providers/irc/IrcServer.hpp \ src/providers/LinkResolver.hpp \ src/providers/twitch/api/Helix.hpp \ diff --git a/src/common/IrcColors.hpp b/src/common/IrcColors.hpp new file mode 100644 index 00000000..97211056 --- /dev/null +++ b/src/common/IrcColors.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +namespace chatterino { + +// Colors taken from https://modern.ircdocs.horse/formatting.html +static QMap IRC_COLORS = { + {0, QColor("white")}, {1, QColor("black")}, + {2, QColor("blue")}, {3, QColor("green")}, + {4, QColor("red")}, {5, QColor("brown")}, + {6, QColor("purple")}, {7, QColor("orange")}, + {8, QColor("yellow")}, {9, QColor("lightgreen")}, + {10, QColor("cyan")}, {11, QColor("lightcyan")}, + {12, QColor("lightblue")}, {13, QColor("pink")}, + {14, QColor("gray")}, {15, QColor("lightgray")}, + {16, QColor("#470000")}, {17, QColor("#472100")}, + {18, QColor("#474700")}, {19, QColor("#324700")}, + {20, QColor("#004700")}, {21, QColor("#00472c")}, + {22, QColor("#004747")}, {23, QColor("#002747")}, + {24, QColor("#000047")}, {25, QColor("#2e0047")}, + {26, QColor("#470047")}, {27, QColor("#47002a")}, + {28, QColor("#740000")}, {29, QColor("#743a00")}, + {30, QColor("#747400")}, {31, QColor("#517400")}, + {32, QColor("#007400")}, {33, QColor("#007449")}, + {34, QColor("#007474")}, {35, QColor("#004074")}, + {36, QColor("#000074")}, {37, QColor("#4b0074")}, + {38, QColor("#740074")}, {39, QColor("#740045")}, + {40, QColor("#b50000")}, {41, QColor("#b56300")}, + {42, QColor("#b5b500")}, {43, QColor("#7db500")}, + {44, QColor("#00b500")}, {45, QColor("#00b571")}, + {46, QColor("#00b5b5")}, {47, QColor("#0063b5")}, + {48, QColor("#0000b5")}, {49, QColor("#7500b5")}, + {50, QColor("#b500b5")}, {51, QColor("#b5006b")}, + {52, QColor("#ff0000")}, {53, QColor("#ff8c00")}, + {54, QColor("#ffff00")}, {55, QColor("#b2ff00")}, + {56, QColor("#00ff00")}, {57, QColor("#00ffa0")}, + {58, QColor("#00ffff")}, {59, QColor("#008cff")}, + {60, QColor("#0000ff")}, {61, QColor("#a500ff")}, + {62, QColor("#ff00ff")}, {63, QColor("#ff0098")}, + {64, QColor("#ff5959")}, {65, QColor("#ffb459")}, + {66, QColor("#ffff71")}, {67, QColor("#cfff60")}, + {68, QColor("#6fff6f")}, {69, QColor("#65ffc9")}, + {70, QColor("#6dffff")}, {71, QColor("#59b4ff")}, + {72, QColor("#5959ff")}, {73, QColor("#c459ff")}, + {74, QColor("#ff66ff")}, {75, QColor("#ff59bc")}, + {76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")}, + {78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")}, + {80, QColor("#9cff9c")}, {81, QColor("#9cffdb")}, + {82, QColor("#9cffff")}, {83, QColor("#9cd3ff")}, + {84, QColor("#9c9cff")}, {85, QColor("#dc9cff")}, + {86, QColor("#ff9cff")}, {87, QColor("#ff94d3")}, + {88, QColor("#000000")}, {89, QColor("#131313")}, + {90, QColor("#282828")}, {91, QColor("#363636")}, + {92, QColor("#4d4d4d")}, {93, QColor("#656565")}, + {94, QColor("#818181")}, {95, QColor("#9f9f9f")}, + {96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")}, + {98, QColor("#ffffff")}, +}; + +} // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 4793ce9d..92ae3d6c 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -34,7 +34,6 @@ struct MessageParseArgs { }; class MessageBuilder - { public: MessageBuilder(); @@ -70,4 +69,5 @@ public: private: std::shared_ptr message_; }; + } // namespace chatterino diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 89c0b1c4..e13f9b25 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,6 +1,7 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" +#include "common/IrcColors.hpp" #include "debug/Benchmark.hpp" #include "messages/Emote.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" @@ -11,6 +12,14 @@ namespace chatterino { +namespace { + + QRegularExpression IRC_COLOR_PARSE_REGEX( + "\u0003(\\d{1,2})?(,(\\d{1,2}))?", + QRegularExpression::UseUnicodePropertiesOption); + +} // namespace + MessageElement::MessageElement(MessageElementFlags flags) : flags_(flags) { @@ -428,4 +437,200 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, } } +// TEXT +// IrcTextElement gets its color from the color code in the message, and can change from character to character. +// This differs from the TextElement +IrcTextElement::IrcTextElement(const QString &fullText, + MessageElementFlags flags, FontStyle style) + : MessageElement(flags) + , style_(style) +{ + assert(IRC_COLOR_PARSE_REGEX.isValid()); + + // Default pen colors. -1 = default theme colors + int fg = -1, bg = -1; + + // Split up the message in words (space separated) + // Each word contains one or more colored segments. + // The color of that segment is "global", as in it can be decided by the word before it. + for (const auto &text : fullText.split(' ')) + { + std::vector segments; + + int pos = 0; + int lastPos = 0; + + auto i = IRC_COLOR_PARSE_REGEX.globalMatch(text); + + while (i.hasNext()) + { + auto match = i.next(); + + if (lastPos != match.capturedStart() && match.capturedStart() != 0) + { + auto seg = Segment{}; + seg.text = text.mid(lastPos, match.capturedStart() - lastPos); + seg.fg = fg; + seg.bg = bg; + segments.emplace_back(seg); + lastPos = match.capturedStart() + match.capturedLength(); + } + + if (!match.captured(1).isEmpty()) + { + fg = match.captured(1).toInt(nullptr); + } + else + { + fg = -1; + } + if (!match.captured(3).isEmpty()) + { + bg = match.captured(3).toInt(nullptr); + } + else if (fg == -1) + { + bg = -1; + } + + lastPos = match.capturedStart() + match.capturedLength(); + } + + auto seg = Segment{}; + seg.text = text.mid(lastPos); + seg.fg = fg; + seg.bg = bg; + segments.emplace_back(seg); + + QString n(text); + + n.replace(IRC_COLOR_PARSE_REGEX, ""); + + Word w{ + n, + -1, + segments, + }; + this->words_.emplace_back(w); + } +} + +void IrcTextElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + auto app = getApp(); + + MessageColor defaultColorType = MessageColor::Text; + auto defaultColor = defaultColorType.getColor(*app->themes); + if (flags.hasAny(this->getFlags())) + { + QFontMetrics metrics = + app->fonts->getFontMetrics(this->style_, container.getScale()); + + for (auto &word : this->words_) + { + auto getTextLayoutElement = [&](QString text, + std::vector segments, + int width, bool hasTrailingSpace) { + std::vector xd{}; + + for (const auto &segment : segments) + { + QColor color = defaultColor; + if (segment.fg >= 0 && segment.fg <= 98) + { + color = IRC_COLORS[segment.fg]; + } + app->themes->normalizeColor(color); + xd.emplace_back(PajSegment{segment.text, color}); + } + + auto e = (new MultiColorTextLayoutElement( + *this, text, QSize(width, metrics.height()), xd, + this->style_, container.getScale())) + ->setLink(this->getLink()); + e->setTrailingSpace(true); + e->setText(text); + + // If URL link was changed, + // Should update it in MessageLayoutElement too! + if (this->getLink().type == Link::Url) + { + static_cast(e)->listenToLinkChanges(); + } + return e; + }; + + // fourtf: add again + // if (word.width == -1) { + word.width = metrics.width(word.text); + // } + + // see if the text fits in the current line + if (container.fitsInLine(word.width)) + { + container.addElementNoLineBreak( + getTextLayoutElement(word.text, word.segments, word.width, + this->hasTrailingSpace())); + continue; + } + + // see if the text fits in the next line + if (!container.atStartOfLine()) + { + container.breakLine(); + + if (container.fitsInLine(word.width)) + { + container.addElementNoLineBreak(getTextLayoutElement( + word.text, word.segments, word.width, + this->hasTrailingSpace())); + continue; + } + } + + // we done goofed, we need to wrap the text + QString text = word.text; + int textLength = text.length(); + int wordStart = 0; + int width = 0; + + // QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1 + + // XXX(pajlada): NOT TESTED + for (int i = 0; i < textLength; i++) // + { + auto isSurrogate = text.size() > i + 1 && + QChar::isHighSurrogate(text[i].unicode()); + + auto charWidth = isSurrogate ? metrics.width(text.mid(i, 2)) + : metrics.width(text[i]); + + if (!container.fitsInLine(width + charWidth)) // + { + container.addElementNoLineBreak(getTextLayoutElement( + text.mid(wordStart, i - wordStart), {}, width, false)); + container.breakLine(); + + wordStart = i; + width = charWidth; + + if (isSurrogate) + i++; + continue; + } + + width += charWidth; + + if (isSurrogate) + i++; + } + + container.addElement(getTextLayoutElement( + text.mid(wordStart), {}, width, this->hasTrailingSpace())); + container.breakLine(); + } + } +} + } // namespace chatterino diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index f5f546df..7a93bbc7 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -291,4 +291,34 @@ public: MessageElementFlags flags) override; }; +// contains a full message string that's split into words on space and parses irc colors that are then put into segments +// these segments are later passed to "MultiColorTextLayoutElement" elements to be rendered :) +class IrcTextElement : public MessageElement +{ +public: + IrcTextElement(const QString &text, MessageElementFlags flags, + FontStyle style = FontStyle::ChatMedium); + ~IrcTextElement() override = default; + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + +private: + FontStyle style_; + + struct Segment { + QString text; + int fg = -1; + int bg = -1; + }; + + struct Word { + QString text; + int width = -1; + std::vector segments; + }; + + std::vector words_; +}; + } // namespace chatterino diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp new file mode 100644 index 00000000..26a2d85a --- /dev/null +++ b/src/messages/SharedMessageBuilder.cpp @@ -0,0 +1,399 @@ +#include "messages/SharedMessageBuilder.hpp" + +#include "Application.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" +#include "messages/Message.hpp" +#include "messages/MessageElement.hpp" +#include "providers/twitch/TwitchCommon.hpp" +#include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" + +namespace chatterino { + +namespace { + + QUrl getFallbackHighlightSound() + { + QString path = getSettings()->pathHighlightSound; + bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile(); + + // Use fallback sound when checkbox is not checked + // or custom file doesn't exist + if (getSettings()->customHighlightSound && fileExists) + { + return QUrl::fromLocalFile(path); + } + else + { + return QUrl("qrc:/sounds/ping2.wav"); + } + } + +} // namespace + +SharedMessageBuilder::SharedMessageBuilder( + Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args) + : channel(_channel) + , ircMessage(_ircMessage) + , args(_args) + , tags(this->ircMessage->tags()) + , originalMessage_(_ircMessage->content()) + , action_(_ircMessage->isAction()) +{ +} + +SharedMessageBuilder::SharedMessageBuilder( + Channel *_channel, const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, QString content, bool isAction) + : channel(_channel) + , ircMessage(_ircMessage) + , args(_args) + , tags(this->ircMessage->tags()) + , originalMessage_(content) + , action_(isAction) +{ +} + +namespace { + + QColor getRandomColor(const QString &v) + { + int colorSeed = 0; + for (const auto &c : v) + { + colorSeed += c.digitValue(); + } + const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size(); + return TWITCH_USERNAME_COLORS[colorIndex]; + } + +} // namespace + +void SharedMessageBuilder::parse() +{ + this->parseUsernameColor(); + + this->parseUsername(); + + this->message().flags.set(MessageFlag::Collapsed); +} + +bool SharedMessageBuilder::isIgnored() const +{ + // TODO(pajlada): Do we need to check if the phrase is valid first? + auto phrases = getCSettings().ignoredMessages.readOnly(); + for (const auto &phrase : *phrases) + { + if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) + { + qDebug() << "Blocking message because it contains ignored phrase" + << phrase.getPattern(); + return true; + } + } + + return false; +} + +void SharedMessageBuilder::parseUsernameColor() +{ + if (getSettings()->colorizeNicknames) + { + this->usernameColor_ = getRandomColor(this->ircMessage->nick()); + } +} + +void SharedMessageBuilder::parseUsername() +{ + // username + this->userName = this->ircMessage->nick(); + + this->message().loginName = this->userName; +} + +void SharedMessageBuilder::parseHighlights() +{ + auto app = getApp(); + + if (this->message().flags.has(MessageFlag::Subscription) && + getSettings()->enableSubHighlight) + { + if (getSettings()->enableSubHighlightTaskbar) + { + this->highlightAlert_ = true; + } + + if (getSettings()->enableSubHighlightSound) + { + this->highlightSound_ = true; + + // Use custom sound if set, otherwise use fallback + if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty()) + { + this->highlightSoundUrl_ = + QUrl(getSettings()->subHighlightSoundUrl.getValue()); + } + else + { + this->highlightSoundUrl_ = getFallbackHighlightSound(); + } + } + + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = + ColorProvider::instance().color(ColorType::Subscription); + + // This message was a subscription. + // Don't check for any other highlight phrases. + return; + } + + // XXX: Non-common term in SharedMessageBuilder + auto currentUser = app->accounts->twitch.getCurrent(); + + QString currentUsername = currentUser->getUserName(); + + if (getCSettings().isBlacklistedUser(this->ircMessage->nick())) + { + // Do nothing. We ignore highlights from this user. + return; + } + + // Highlight because it's a whisper + if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight) + { + if (getSettings()->enableWhisperHighlightTaskbar) + { + this->highlightAlert_ = true; + } + + if (getSettings()->enableWhisperHighlightSound) + { + this->highlightSound_ = true; + + // Use custom sound if set, otherwise use fallback + if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty()) + { + this->highlightSoundUrl_ = + QUrl(getSettings()->whisperHighlightSoundUrl.getValue()); + } + else + { + this->highlightSoundUrl_ = getFallbackHighlightSound(); + } + } + + this->message().highlightColor = + ColorProvider::instance().color(ColorType::Whisper); + + /* + * Do _NOT_ return yet, we might want to apply phrase/user name + * highlights (which override whisper color/sound). + */ + } + + // Highlight because of sender + auto userHighlights = getCSettings().highlightedUsers.readOnly(); + for (const HighlightPhrase &userHighlight : *userHighlights) + { + if (!userHighlight.isMatch(this->ircMessage->nick())) + { + continue; + } + qDebug() << "Highlight because user" << this->ircMessage->nick() + << "sent a message"; + + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = userHighlight.getColor(); + + if (userHighlight.hasAlert()) + { + this->highlightAlert_ = true; + } + + if (userHighlight.hasSound()) + { + this->highlightSound_ = true; + // Use custom sound if set, otherwise use the fallback sound + if (userHighlight.hasCustomSound()) + { + this->highlightSoundUrl_ = userHighlight.getSoundUrl(); + } + else + { + this->highlightSoundUrl_ = getFallbackHighlightSound(); + } + } + + if (this->highlightAlert_ && this->highlightSound_) + { + /* + * User name highlights "beat" highlight phrases: If a message has + * all attributes (color, taskbar flashing, sound) set, highlight + * phrases will not be checked. + */ + return; + } + } + + if (this->ircMessage->nick() == currentUsername) + { + // Do nothing. Highlights cannot be triggered by yourself + return; + } + + // TODO: This vector should only be rebuilt upon highlights being changed + // fourtf: should be implemented in the HighlightsController + std::vector activeHighlights = + getSettings()->highlightedMessages.cloneVector(); + + if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) + { + HighlightPhrase selfHighlight( + currentUsername, getSettings()->enableSelfHighlightTaskbar, + getSettings()->enableSelfHighlightSound, false, false, + getSettings()->selfHighlightSoundUrl.getValue(), + ColorProvider::instance().color(ColorType::SelfHighlight)); + activeHighlights.emplace_back(std::move(selfHighlight)); + } + + // Highlight because of message + for (const HighlightPhrase &highlight : activeHighlights) + { + if (!highlight.isMatch(this->originalMessage_)) + { + continue; + } + + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = highlight.getColor(); + + if (highlight.hasAlert()) + { + this->highlightAlert_ = true; + } + + // Only set highlightSound_ if it hasn't been set by username + // highlights already. + if (highlight.hasSound() && !this->highlightSound_) + { + this->highlightSound_ = true; + + // Use custom sound if set, otherwise use fallback sound + if (highlight.hasCustomSound()) + { + this->highlightSoundUrl_ = highlight.getSoundUrl(); + } + else + { + this->highlightSoundUrl_ = getFallbackHighlightSound(); + } + } + + if (this->highlightAlert_ && this->highlightSound_) + { + /* + * Break once no further attributes (taskbar, sound) can be + * applied. + */ + break; + } + } +} + +void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote) +{ + this->emplace(emote, MessageElementFlag::EmojiAll); +} + +void SharedMessageBuilder::addTextOrEmoji(const QString &string_) +{ + auto string = QString(string_); + + // Actually just text + auto linkString = this->matchLink(string); + auto link = Link(); + auto textColor = this->action_ ? MessageColor(this->usernameColor_) + : MessageColor(MessageColor::Text); + + if (linkString.isEmpty()) + { + if (string.startsWith('@')) + { + this->emplace(string, MessageElementFlag::BoldUsername, + textColor, FontStyle::ChatMediumBold); + this->emplace( + string, MessageElementFlag::NonBoldUsername, textColor); + } + else + { + this->emplace(string, MessageElementFlag::Text, + textColor); + } + } + else + { + this->addLink(string, linkString); + } +} + +void SharedMessageBuilder::appendChannelName() +{ + QString channelName("#" + this->channel->getName()); + Link link(Link::Url, this->channel->getName() + "\n" + this->message().id); + + this->emplace(channelName, MessageElementFlag::ChannelName, + MessageColor::System) // + ->setLink(link); +} + +inline QMediaPlayer *getPlayer() +{ + if (isGuiThread()) + { + static auto player = new QMediaPlayer; + return player; + } + else + { + return nullptr; + } +} + +void SharedMessageBuilder::triggerHighlights() +{ + static QUrl currentPlayerUrl; + + if (getCSettings().isMutedChannel(this->channel->getName())) + { + // Do nothing. Pings are muted in this channel. + return; + } + + bool hasFocus = (QApplication::focusWidget() != nullptr); + bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound; + + if (this->highlightSound_ && resolveFocus) + { + if (auto player = getPlayer()) + { + // update the media player url if necessary + if (currentPlayerUrl != this->highlightSoundUrl_) + { + player->setMedia(this->highlightSoundUrl_); + + currentPlayerUrl = this->highlightSoundUrl_; + } + + player->play(); + } + } + + if (this->highlightAlert_) + { + getApp()->windows->sendAlert(); + } +} + +} // namespace chatterino diff --git a/src/messages/SharedMessageBuilder.hpp b/src/messages/SharedMessageBuilder.hpp new file mode 100644 index 00000000..355c2908 --- /dev/null +++ b/src/messages/SharedMessageBuilder.hpp @@ -0,0 +1,69 @@ +#include "messages/MessageBuilder.hpp" + +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" + +#include +#include + +namespace chatterino { + +class SharedMessageBuilder : public MessageBuilder +{ +public: + SharedMessageBuilder() = delete; + + explicit SharedMessageBuilder(Channel *_channel, + const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args); + + explicit SharedMessageBuilder(Channel *_channel, + const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, + QString content, bool isAction); + + QString userName; + + [[nodiscard]] virtual bool isIgnored() const; + + // triggerHighlights triggers any alerts or sounds parsed by parseHighlights + virtual void triggerHighlights(); + virtual MessagePtr build() = 0; + +protected: + virtual void parse(); + + virtual void parseUsernameColor(); + + virtual void parseUsername(); + + virtual Outcome tryAppendEmote(const EmoteName &name) + { + return Failure; + } + + // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function + virtual void parseHighlights(); + + virtual void addTextOrEmoji(EmotePtr emote); + virtual void addTextOrEmoji(const QString &value); + + void appendChannelName(); + + Channel *channel; + const Communi::IrcMessage *ircMessage; + MessageParseArgs args; + const QVariantMap tags; + QString originalMessage_; + + const bool action_{}; + + QColor usernameColor_; + + bool highlightAlert_ = false; + bool highlightSound_ = false; + + QUrl highlightSoundUrl_; +}; + +} // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 7990d6a6..49edde0e 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index) } } +// +// TEXT +// + +MultiColorTextLayoutElement::MultiColorTextLayoutElement( + MessageElement &_creator, QString &_text, const QSize &_size, + std::vector segments, FontStyle _style, float _scale) + : TextLayoutElement(_creator, _text, _size, QColor{}, _style, _scale) + , segments_(segments) +{ + this->setText(_text); +} + +void MultiColorTextLayoutElement::paint(QPainter &painter) +{ + auto app = getApp(); + + painter.setPen(this->color_); + + painter.setFont(app->fonts->getFont(this->style_, this->scale_)); + + int xOffset = 0; + + auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_); + + for (const auto &segment : this->segments_) + { + // qDebug() << "Draw segment:" << segment.text; + painter.setPen(segment.color); + painter.drawText(QRectF(this->getRect().x() + xOffset, + this->getRect().y(), 10000, 10000), + segment.text, + QTextOption(Qt::AlignLeft | Qt::AlignTop)); + xOffset += metrics.width(segment.text); + } +} + } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 60d166a4..fd711af8 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -107,7 +107,6 @@ protected: int getMouseOverIndex(const QPoint &abs) const override; int getXFromIndex(int index) override; -private: QColor color_; FontStyle style_; float scale_; @@ -138,4 +137,25 @@ private: QString line2; }; +struct PajSegment { + QString text; + QColor color; +}; + +// TEXT +class MultiColorTextLayoutElement : public TextLayoutElement +{ +public: + MultiColorTextLayoutElement(MessageElement &creator_, QString &text, + const QSize &size, + std::vector segments, + FontStyle style_, float scale_); + +protected: + void paint(QPainter &painter) override; + +private: + std::vector segments_; +}; + } // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp new file mode 100644 index 00000000..c7529a5e --- /dev/null +++ b/src/providers/irc/IrcMessageBuilder.cpp @@ -0,0 +1,93 @@ +#include "providers/irc/IrcMessageBuilder.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/ignores/IgnoreController.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" +#include "messages/Message.hpp" +#include "providers/chatterino/ChatterinoBadges.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" +#include "util/IrcHelpers.hpp" +#include "widgets/Window.hpp" + +namespace chatterino { + +IrcMessageBuilder::IrcMessageBuilder( + Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args) + : SharedMessageBuilder(_channel, _ircMessage, _args) +{ + this->usernameColor_ = getApp()->themes->messages.textColors.system; +} + +IrcMessageBuilder::IrcMessageBuilder(Channel *_channel, + const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, + QString content, bool isAction) + : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) +{ + assert(false); + this->usernameColor_ = getApp()->themes->messages.textColors.system; +} + +MessagePtr IrcMessageBuilder::build() +{ + // PARSE + this->parse(); + + // PUSH ELEMENTS + this->appendChannelName(); + + this->emplace(); + + this->appendUsername(); + + // words + this->addWords(this->originalMessage_.split(' ')); + + this->message().messageText = this->originalMessage_; + this->message().searchText = this->message().localizedName + " " + + this->userName + ": " + this->originalMessage_; + + // highlights + this->parseHighlights(); + + // highlighting incoming whispers if requested per setting + if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) + { + this->message().flags.set(MessageFlag::HighlightedWhisper, true); + } + + return this->release(); +} + +void IrcMessageBuilder::addWords(const QStringList &words) +{ + this->emplace(words.join(' '), MessageElementFlag::Text); +} + +void IrcMessageBuilder::appendUsername() +{ + auto app = getApp(); + + QString username = this->userName; + this->message().loginName = username; + + // The full string that will be rendered in the chat widget + QString usernameText = username; + + if (!this->action_) + { + usernameText += ":"; + } + + this->emplace(usernameText, MessageElementFlag::Username, + this->usernameColor_, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, this->message().displayName}); +} + +} // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.hpp b/src/providers/irc/IrcMessageBuilder.hpp new file mode 100644 index 00000000..d6a83da8 --- /dev/null +++ b/src/providers/irc/IrcMessageBuilder.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" +#include "messages/SharedMessageBuilder.hpp" +#include "providers/twitch/TwitchBadge.hpp" + +#include +#include +#include + +namespace chatterino { + +struct Emote; +using EmotePtr = std::shared_ptr; + +class Channel; +class TwitchChannel; + +class IrcMessageBuilder : public SharedMessageBuilder +{ +public: + IrcMessageBuilder() = delete; + + explicit IrcMessageBuilder(Channel *_channel, + const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args); + explicit IrcMessageBuilder(Channel *_channel, + const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, QString content, + bool isAction); + + MessagePtr build() override; + +private: + void appendUsername(); + + void addWords(const QStringList &words); +}; + +} // namespace chatterino diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 1b3aea5f..006e55ac 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -4,9 +4,9 @@ #include #include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" #include "providers/irc/Irc2.hpp" #include "providers/irc/IrcChannel2.hpp" +#include "providers/irc/IrcMessageBuilder.hpp" #include "singletons/Settings.hpp" #include "util/QObjectRef.hpp" @@ -86,6 +86,7 @@ void IrcServer::initializeConnectionSignals(IrcConnection *connection, QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived, this, [this](Communi::IrcNoticeMessage *message) { + // XD PAJLADA MessageBuilder builder; builder.emplace(); @@ -176,15 +177,18 @@ void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) if (auto channel = this->getChannelOrEmpty(target); !channel->isEmpty()) { - MessageBuilder builder; + MessageParseArgs args; + IrcMessageBuilder builder(channel.get(), message, args); - builder.emplace(); - builder.emplace(message->nick() + ":", - MessageElementFlag::Username); - builder.emplace(message->content(), - MessageElementFlag::Text); - - channel->addMessage(builder.release()); + if (!builder.isIgnored()) + { + builder.triggerHighlights(); + channel->addMessage(builder.build()); + } + else + { + qDebug() << "message ignored :rage:"; + } } } @@ -205,8 +209,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) { if (message->nick() == this->data_->nick) { - shared->addMessage( - MessageBuilder(systemMessage, "joined").release()); + shared->addMessage(makeSystemMessage("joined")); } else { @@ -230,8 +233,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) { if (message->nick() == this->data_->nick) { - shared->addMessage( - MessageBuilder(systemMessage, "parted").release()); + shared->addMessage(makeSystemMessage("parted")); } else { diff --git a/src/providers/twitch/TwitchCommon.hpp b/src/providers/twitch/TwitchCommon.hpp index af9db84f..329da107 100644 --- a/src/providers/twitch/TwitchCommon.hpp +++ b/src/providers/twitch/TwitchCommon.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -11,4 +12,22 @@ inline QByteArray getDefaultClientID() return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal"); } +static const std::vector TWITCH_USERNAME_COLORS = { + {255, 0, 0}, // Red + {0, 0, 255}, // Blue + {0, 255, 0}, // Green + {178, 34, 34}, // FireBrick + {255, 127, 80}, // Coral + {154, 205, 50}, // YellowGreen + {255, 69, 0}, // OrangeRed + {46, 139, 87}, // SeaGreen + {218, 165, 32}, // GoldenRod + {210, 105, 30}, // Chocolate + {95, 158, 160}, // CadetBlue + {30, 144, 255}, // DodgerBlue + {255, 105, 180}, // HotPink + {138, 43, 226}, // BlueViolet + {0, 255, 127}, // SpringGreen +}; + } // namespace chatterino diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 0f1d4884..b2c2fe98 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -14,7 +14,6 @@ #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchHelpers.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" #include "util/PostToThread.hpp" // using namespace Communi; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 6684938d..ed587f78 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -8,6 +8,7 @@ #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" @@ -31,65 +32,27 @@ const QSet zeroWidthEmotes{ "ReinDeer", "CandyCane", "cvMask", "cvHazmat", }; -QColor getRandomColor(const QVariant &userId) -{ - static const std::vector twitchUsernameColors = { - {255, 0, 0}, // Red - {0, 0, 255}, // Blue - {0, 255, 0}, // Green - {178, 34, 34}, // FireBrick - {255, 127, 80}, // Coral - {154, 205, 50}, // YellowGreen - {255, 69, 0}, // OrangeRed - {46, 139, 87}, // SeaGreen - {218, 165, 32}, // GoldenRod - {210, 105, 30}, // Chocolate - {95, 158, 160}, // CadetBlue - {30, 144, 255}, // DodgerBlue - {255, 105, 180}, // HotPink - {138, 43, 226}, // BlueViolet - {0, 255, 127}, // SpringGreen - }; - - bool ok = true; - int colorSeed = userId.toInt(&ok); - if (!ok) - { - // We were unable to convert the user ID to an integer, this means Twitch has decided to start using non-integer user IDs - // Just randomize the users color - colorSeed = std::rand(); - } - - assert(twitchUsernameColors.size() != 0); - const auto colorIndex = colorSeed % twitchUsernameColors.size(); - return twitchUsernameColors[colorIndex]; -} - -QUrl getFallbackHighlightSound() -{ - using namespace chatterino; - - QString path = getSettings()->pathHighlightSound; - bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile(); - - // Use fallback sound when checkbox is not checked - // or custom file doesn't exist - if (getSettings()->customHighlightSound && fileExists) - { - return QUrl::fromLocalFile(path); - } - else - { - return QUrl("qrc:/sounds/ping2.wav"); - } -} - } // namespace namespace chatterino { namespace { + QColor getRandomColor(const QVariant &userId) + { + bool ok = true; + int colorSeed = userId.toInt(&ok); + if (!ok) + { + // We were unable to convert the user ID to an integer, this means Twitch has decided to start using non-integer user IDs + // Just randomize the users color + colorSeed = std::rand(); + } + + const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size(); + return TWITCH_USERNAME_COLORS[colorIndex]; + } + QStringList parseTagList(const QVariantMap &tags, const QString &key) { auto iterator = tags.find(key); @@ -141,13 +104,8 @@ namespace { TwitchMessageBuilder::TwitchMessageBuilder( Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, const MessageParseArgs &_args) - : channel(_channel) + : SharedMessageBuilder(_channel, _ircMessage, _args) , twitchChannel(dynamic_cast(_channel)) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(_ircMessage->content()) - , action_(_ircMessage->isAction()) { this->usernameColor_ = getApp()->themes->messages.textColors.system; } @@ -155,31 +113,21 @@ TwitchMessageBuilder::TwitchMessageBuilder( TwitchMessageBuilder::TwitchMessageBuilder( Channel *_channel, const Communi::IrcMessage *_ircMessage, const MessageParseArgs &_args, QString content, bool isAction) - : channel(_channel) + : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) , twitchChannel(dynamic_cast(_channel)) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(content) - , action_(isAction) { this->usernameColor_ = getApp()->themes->messages.textColors.system; } bool TwitchMessageBuilder::isIgnored() const { - auto app = getApp(); - - // TODO(pajlada): Do we need to check if the phrase is valid first? - auto phrases = getCSettings().ignoredMessages.readOnly(); - for (const auto &phrase : *phrases) + if (SharedMessageBuilder::isIgnored()) { - if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) - { - return true; - } + return true; } + auto app = getApp(); + if (getSettings()->enableTwitchIgnoredUsers && this->tags.contains("user-id")) { @@ -214,75 +162,29 @@ bool TwitchMessageBuilder::isIgnored() const return false; } -inline QMediaPlayer *getPlayer() -{ - if (isGuiThread()) - { - static auto player = new QMediaPlayer; - return player; - } - else - { - return nullptr; - } -} - void TwitchMessageBuilder::triggerHighlights() { - static QUrl currentPlayerUrl; - if (this->historicalMessage_) { // Do nothing. Highlights should not be triggered on historical messages. return; } - if (getCSettings().isMutedChannel(this->channel->getName())) - { - // Do nothing. Pings are muted in this channel. - return; - } - - bool hasFocus = (QApplication::focusWidget() != nullptr); - bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound; - - if (this->highlightSound_ && resolveFocus) - { - if (auto player = getPlayer()) - { - // update the media player url if necessary - if (currentPlayerUrl != this->highlightSoundUrl_) - { - player->setMedia(this->highlightSoundUrl_); - - currentPlayerUrl = this->highlightSoundUrl_; - } - - player->play(); - } - } - - if (this->highlightAlert_) - { - getApp()->windows->sendAlert(); - } + SharedMessageBuilder::triggerHighlights(); } MessagePtr TwitchMessageBuilder::build() { - // PARSING + // PARSE this->userId_ = this->ircMessage->tag("user-id").toString(); - this->parseUsername(); + this->parse(); if (this->userName == this->channel->getName()) { this->senderIsBroadcaster = true; } - this->message().flags.set(MessageFlag::Collapsed); - - // PARSING this->parseMessageID(); this->parseRoomID(); @@ -479,7 +381,7 @@ void TwitchMessageBuilder::addWords( void TwitchMessageBuilder::addTextOrEmoji(EmotePtr emote) { - this->emplace(emote, MessageElementFlag::EmojiAll); + return SharedMessageBuilder::addTextOrEmoji(emote); } void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) @@ -528,40 +430,6 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) { this->addLink(string, linkString); } - - // if (!linkString.isEmpty()) { - // if (getSettings()->lowercaseLink) { - // QRegularExpression httpRegex("\\bhttps?://", - // QRegularExpression::CaseInsensitiveOption); QRegularExpression - // ftpRegex("\\bftps?://", - // QRegularExpression::CaseInsensitiveOption); QRegularExpression - // getDomain("\\/\\/([^\\/]*)"); QString tempString = string; - - // if (!string.contains(httpRegex)) { - // if (!string.contains(ftpRegex)) { - // tempString.insert(0, "http://"); - // } - // } - // QString domain = getDomain.match(tempString).captured(1); - // string.replace(domain, domain.toLower()); - // } - // link = Link(Link::Url, linkString); - // textColor = MessageColor(MessageColor::Link); - //} - // if (string.startsWith('@')) { - // this->emplace(string, MessageElementFlag::BoldUsername, - // textColor, - // FontStyle::ChatMediumBold) // - // ->setLink(link); - // this->emplace(string, - // MessageElementFlag::NonBoldUsername, - // textColor) // - // ->setLink(link); - //} else { - // this->emplace(string, MessageElementFlag::Text, - // textColor) // - // ->setLink(link); - //} } void TwitchMessageBuilder::parseMessageID() @@ -594,16 +462,6 @@ void TwitchMessageBuilder::parseRoomID() } } -void TwitchMessageBuilder::appendChannelName() -{ - QString channelName("#" + this->channel->getName()); - Link link(Link::Url, this->channel->getName() + "\n" + this->message().id); - - this->emplace(channelName, MessageElementFlag::ChannelName, - MessageColor::System) // - ->setLink(link); -} - void TwitchMessageBuilder::parseUsernameColor() { const auto iterator = this->tags.find("color"); @@ -624,10 +482,7 @@ void TwitchMessageBuilder::parseUsernameColor() void TwitchMessageBuilder::parseUsername() { - this->parseUsernameColor(); - - // username - this->userName = this->ircMessage->nick(); + SharedMessageBuilder::parseUsername(); if (this->userName.isEmpty() || this->args.trimSubscriberUsername) { @@ -983,193 +838,6 @@ void TwitchMessageBuilder::runIgnoreReplaces( } } -void TwitchMessageBuilder::parseHighlights() -{ - auto app = getApp(); - - if (this->message().flags.has(MessageFlag::Subscription) && - getSettings()->enableSubHighlight) - { - if (getSettings()->enableSubHighlightTaskbar) - { - this->highlightAlert_ = true; - } - - if (getSettings()->enableSubHighlightSound) - { - this->highlightSound_ = true; - - // Use custom sound if set, otherwise use fallback - if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty()) - { - this->highlightSoundUrl_ = - QUrl(getSettings()->subHighlightSoundUrl.getValue()); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - } - - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Subscription); - - // This message was a subscription. - // Don't check for any other highlight phrases. - return; - } - - auto currentUser = app->accounts->twitch.getCurrent(); - - QString currentUsername = currentUser->getUserName(); - - if (getCSettings().isBlacklistedUser(this->ircMessage->nick())) - { - // Do nothing. We ignore highlights from this user. - return; - } - - // Highlight because it's a whisper - if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight) - { - if (getSettings()->enableWhisperHighlightTaskbar) - { - this->highlightAlert_ = true; - } - - if (getSettings()->enableWhisperHighlightSound) - { - this->highlightSound_ = true; - - // Use custom sound if set, otherwise use fallback - if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty()) - { - this->highlightSoundUrl_ = - QUrl(getSettings()->whisperHighlightSoundUrl.getValue()); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - } - - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Whisper); - - /* - * Do _NOT_ return yet, we might want to apply phrase/user name - * highlights (which override whisper color/sound). - */ - } - - // Highlight because of sender - auto userHighlights = getCSettings().highlightedUsers.readOnly(); - for (const HighlightPhrase &userHighlight : *userHighlights) - { - if (!userHighlight.isMatch(this->ircMessage->nick())) - { - continue; - } - - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = userHighlight.getColor(); - - if (userHighlight.hasAlert()) - { - this->highlightAlert_ = true; - } - - if (userHighlight.hasSound()) - { - this->highlightSound_ = true; - // Use custom sound if set, otherwise use the fallback sound - if (userHighlight.hasCustomSound()) - { - this->highlightSoundUrl_ = userHighlight.getSoundUrl(); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - } - - if (this->highlightAlert_ && this->highlightSound_) - { - /* - * User name highlights "beat" highlight phrases: If a message has - * all attributes (color, taskbar flashing, sound) set, highlight - * phrases will not be checked. - */ - return; - } - } - - if (this->ircMessage->nick() == currentUsername) - { - // Do nothing. Highlights cannot be triggered by yourself - return; - } - - // TODO: This vector should only be rebuilt upon highlights being changed - // fourtf: should be implemented in the HighlightsController - std::vector activeHighlights = - getSettings()->highlightedMessages.cloneVector(); - - if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) - { - HighlightPhrase selfHighlight( - currentUsername, getSettings()->enableSelfHighlightTaskbar, - getSettings()->enableSelfHighlightSound, false, false, - getSettings()->selfHighlightSoundUrl.getValue(), - ColorProvider::instance().color(ColorType::SelfHighlight)); - activeHighlights.emplace_back(std::move(selfHighlight)); - } - - // Highlight because of message - for (const HighlightPhrase &highlight : activeHighlights) - { - if (!highlight.isMatch(this->originalMessage_)) - { - continue; - } - - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = highlight.getColor(); - - if (highlight.hasAlert()) - { - this->highlightAlert_ = true; - } - - // Only set highlightSound_ if it hasn't been set by username - // highlights already. - if (highlight.hasSound() && !this->highlightSound_) - { - this->highlightSound_ = true; - - // Use custom sound if set, otherwise use fallback sound - if (highlight.hasCustomSound()) - { - this->highlightSoundUrl_ = highlight.getSoundUrl(); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - } - - if (this->highlightAlert_ && this->highlightSound_) - { - /* - * Break once no further attributes (taskbar, sound) can be - * applied. - */ - break; - } - } -} - void TwitchMessageBuilder::appendTwitchEmote( const QString &emote, std::vector> &vec, diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 7066c326..4bd72f14 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -2,7 +2,7 @@ #include "common/Aliases.hpp" #include "common/Outcome.hpp" -#include "messages/MessageBuilder.hpp" +#include "messages/SharedMessageBuilder.hpp" #include "providers/twitch/TwitchBadge.hpp" #include @@ -17,7 +17,7 @@ using EmotePtr = std::shared_ptr; class Channel; class TwitchChannel; -class TwitchMessageBuilder : public MessageBuilder +class TwitchMessageBuilder : public SharedMessageBuilder { public: enum UsernameDisplayMode : int { @@ -36,30 +36,20 @@ public: const MessageParseArgs &_args, QString content, bool isAction); - Channel *channel; TwitchChannel *twitchChannel; - const Communi::IrcMessage *ircMessage; - MessageParseArgs args; - const QVariantMap tags; - QString userName; - - [[nodiscard]] bool isIgnored() const; - // triggerHighlights triggers any alerts or sounds parsed by parseHighlights - void triggerHighlights(); - MessagePtr build(); + [[nodiscard]] bool isIgnored() const override; + void triggerHighlights() override; + MessagePtr build() override; private: + void parseUsernameColor() override; + void parseUsername() override; void parseMessageID(); void parseRoomID(); - void appendChannelName(); - void parseUsernameColor(); - void parseUsername(); void appendUsername(); void runIgnoreReplaces( std::vector> &twitchEmotes); - // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function - void parseHighlights(); boost::optional getTwitchBadge(const Badge &badge); void appendTwitchEmote( @@ -71,8 +61,8 @@ private: void addWords( const QStringList &words, const std::vector> &twitchEmotes); - void addTextOrEmoji(EmotePtr emote); - void addTextOrEmoji(const QString &value); + void addTextOrEmoji(EmotePtr emote) override; + void addTextOrEmoji(const QString &value) override; void appendTwitchBadges(); void appendChatterinoBadges(); @@ -86,16 +76,7 @@ private: bool historicalMessage_ = false; QString userId_; - QColor usernameColor_; - QString originalMessage_; bool senderIsBroadcaster{}; - - const bool action_ = false; - - bool highlightAlert_ = false; - bool highlightSound_ = false; - - QUrl highlightSoundUrl_; }; } // namespace chatterino