#include "controllers/highlights/HighlightController.hpp" #include "Application.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightBadge.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "providers/colors/ColorProvider.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" namespace { using namespace chatterino; 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) -> std::optional { (void)args; // unused (void)badges; // unused (void)senderName; // unused (void)flags; // unused if (self) { // Phrase checks should ignore highlights from the user return std::nullopt; } if (!highlight.isMatch(originalMessage)) { return std::nullopt; } std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); } return HighlightResult{ highlight.hasAlert(), highlight.hasSound(), highlightSoundUrl, highlight.getColor(), highlight.showInMentions(), }; }}; } void rebuildSubscriptionHighlights(Settings &settings, std::vector &checks) { if (settings.enableSubHighlight) { auto highlightSound = settings.enableSubHighlightSound.getValue(); auto highlightAlert = settings.enableSubHighlightTaskbar.getValue(); auto highlightSoundUrlValue = settings.subHighlightSoundUrl.getValue(); std::optional highlightSoundUrl; if (!highlightSoundUrlValue.isEmpty()) { highlightSoundUrl = highlightSoundUrlValue; } // The custom sub highlight color is handled in ColorProvider checks.emplace_back(HighlightCheck{ [=](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, const auto &flags, const auto self) -> std::optional { (void)badges; // unused (void)senderName; // unused (void)originalMessage; // unused (void)flags; // unused (void)self; // unused if (!args.isSubscriptionMessage) { return std::nullopt; } auto highlightColor = ColorProvider::instance().color(ColorType::Subscription); return HighlightResult{ highlightAlert, // alert highlightSound, // playSound highlightSoundUrl, // customSoundUrl highlightColor, // color false, // showInMentions }; }}); } } void rebuildWhisperHighlights(Settings &settings, std::vector &checks) { if (settings.enableWhisperHighlight) { auto highlightSound = settings.enableWhisperHighlightSound.getValue(); auto highlightAlert = settings.enableWhisperHighlightTaskbar.getValue(); auto highlightSoundUrlValue = settings.whisperHighlightSoundUrl.getValue(); std::optional highlightSoundUrl; if (!highlightSoundUrlValue.isEmpty()) { highlightSoundUrl = highlightSoundUrlValue; } // The custom whisper highlight color is handled in ColorProvider checks.emplace_back(HighlightCheck{ [=](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, const auto &flags, const auto self) -> std::optional { (void)badges; // unused (void)senderName; // unused (void)originalMessage; // unused (void)flags; // unused (void)self; // unused if (!args.isReceivedWhisper) { return std::nullopt; } return HighlightResult{ highlightAlert, highlightSound, highlightSoundUrl, ColorProvider::instance().color(ColorType::Whisper), false, }; }}); } } void rebuildReplyThreadHighlight(Settings &settings, std::vector &checks) { if (settings.enableThreadHighlight) { auto highlightSound = settings.enableThreadHighlightSound.getValue(); auto highlightAlert = settings.enableThreadHighlightTaskbar.getValue(); auto highlightSoundUrlValue = settings.threadHighlightSoundUrl.getValue(); std::optional 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) -> std::optional { if (flags.has(MessageFlag::SubscribedThread) && !self) { return HighlightResult{ highlightAlert, highlightSound, highlightSoundUrl, ColorProvider::instance().color( ColorType::ThreadMessageHighlight), highlightInMentions, }; } return std::nullopt; }}); } } void rebuildMessageHighlights(Settings &settings, std::vector &checks) { auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); QString currentUsername = currentUser->getUserName(); if (settings.enableSelfHighlight && !currentUsername.isEmpty() && !currentUser->isAnon()) { HighlightPhrase highlight( currentUsername, settings.showSelfHighlightInMentions, settings.enableSelfHighlightTaskbar, settings.enableSelfHighlightSound, false, false, settings.selfHighlightSoundUrl.getValue(), ColorProvider::instance().color(ColorType::SelfHighlight)); checks.emplace_back(highlightPhraseCheck(highlight)); } auto messageHighlights = settings.highlightedMessages.readOnly(); for (const auto &highlight : *messageHighlights) { checks.emplace_back(highlightPhraseCheck(highlight)); } if (settings.enableAutomodHighlight) { const auto highlightSound = settings.enableAutomodHighlightSound.getValue(); const auto highlightAlert = settings.enableAutomodHighlightTaskbar.getValue(); const auto highlightSoundUrlValue = settings.automodHighlightSoundUrl.getValue(); auto highlightColor = ColorProvider::instance().color(ColorType::AutomodHighlight); checks.emplace_back(HighlightCheck{ [=](const auto & /*args*/, const auto & /*badges*/, const auto & /*senderName*/, const auto & /*originalMessage*/, const auto &flags, const auto /*self*/) -> std::optional { if (!flags.has(MessageFlag::AutoModOffendingMessage)) { return std::nullopt; } std::optional highlightSoundUrl; if (!highlightSoundUrlValue.isEmpty()) { highlightSoundUrl = highlightSoundUrlValue; } return HighlightResult{ highlightAlert, // alert highlightSound, // playSound highlightSoundUrl, // customSoundUrl highlightColor, // color false, // showInMentions }; }}); } } void rebuildUserHighlights(Settings &settings, std::vector &checks) { auto userHighlights = settings.highlightedUsers.readOnly(); if (settings.enableSelfMessageHighlight) { bool showInMentions = settings.showSelfMessageHighlightInMentions; checks.emplace_back(HighlightCheck{ [showInMentions]( const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, const auto &flags, const auto self) -> std::optional { (void)args; //unused (void)badges; //unused (void)senderName; //unused (void)flags; //unused (void)originalMessage; //unused if (!self) { return std::nullopt; } // Highlight color is provided by the ColorProvider and will be updated accordingly auto highlightColor = ColorProvider::instance().color( ColorType::SelfMessageHighlight); return HighlightResult{false, false, (QUrl) nullptr, highlightColor, showInMentions}; }}); } for (const auto &highlight : *userHighlights) { checks.emplace_back(HighlightCheck{ [highlight](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, const auto &flags, const auto self) -> std::optional { (void)args; // unused (void)badges; // unused (void)originalMessage; // unused (void)flags; // unused (void)self; // unused if (!highlight.isMatch(senderName)) { return std::nullopt; } std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); } return HighlightResult{ highlight.hasAlert(), // highlight.hasSound(), // highlightSoundUrl, // highlight.getColor(), // highlight.showInMentions(), // }; }}); } } void rebuildBadgeHighlights(Settings &settings, std::vector &checks) { auto badgeHighlights = settings.highlightedBadges.readOnly(); for (const auto &highlight : *badgeHighlights) { checks.emplace_back(HighlightCheck{ [highlight](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, const auto &flags, const auto self) -> std::optional { (void)args; // unused (void)senderName; // unused (void)originalMessage; // unused (void)flags; // unused (void)self; // unused for (const Badge &badge : badges) { if (highlight.isMatch(badge)) { std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); } return HighlightResult{ highlight.hasAlert(), // highlight.hasSound(), // highlightSoundUrl, // highlight.getColor(), // highlight.showInMentions(), // }; } } return std::nullopt; }}); } } } // namespace namespace chatterino { HighlightResult::HighlightResult(bool _alert, bool _playSound, std::optional _customSoundUrl, std::shared_ptr _color, bool _showInMentions) : alert(_alert) , playSound(_playSound) , customSoundUrl(std::move(_customSoundUrl)) , color(std::move(_color)) , showInMentions(_showInMentions) { } HighlightResult HighlightResult::emptyResult() { return { false, false, std::nullopt, nullptr, false, }; } bool HighlightResult::operator==(const HighlightResult &other) const { if (this->alert != other.alert) { return false; } if (this->playSound != other.playSound) { return false; } if (this->customSoundUrl != other.customSoundUrl) { return false; } if (this->color && other.color) { if (*this->color != *other.color) { return false; } } if (this->showInMentions != other.showInMentions) { return false; } return true; } bool HighlightResult::operator!=(const HighlightResult &other) const { return !(*this == other); } bool HighlightResult::empty() const { return !this->alert && !this->playSound && !this->customSoundUrl.has_value() && !this->color && !this->showInMentions; } bool HighlightResult::full() const { return this->alert && this->playSound && this->customSoundUrl.has_value() && this->color && this->showInMentions; } std::ostream &operator<<(std::ostream &os, const HighlightResult &result) { os << "Alert: " << (result.alert ? "Yes" : "No") << ", " << "Play sound: " << (result.playSound ? "Yes" : "No") << " (" << (result.customSoundUrl ? result.customSoundUrl->toString().toStdString() : "") << ")" << ", " << "Color: " << (result.color ? result.color->name().toStdString() : "") << ", " << "Show in mentions: " << (result.showInMentions ? "Yes" : "No"); return os; } HighlightController::HighlightController(Settings &settings, AccountController *accounts) { assert(accounts != nullptr); this->rebuildListener_.addSetting(settings.enableSelfHighlight); this->rebuildListener_.addSetting(settings.enableSelfHighlightSound); this->rebuildListener_.addSetting(settings.enableSelfHighlightTaskbar); this->rebuildListener_.addSetting(settings.selfHighlightSoundUrl); this->rebuildListener_.addSetting(settings.showSelfHighlightInMentions); this->rebuildListener_.addSetting(settings.enableWhisperHighlight); this->rebuildListener_.addSetting(settings.enableWhisperHighlightSound); this->rebuildListener_.addSetting(settings.enableWhisperHighlightTaskbar); this->rebuildListener_.addSetting(settings.whisperHighlightSoundUrl); this->rebuildListener_.addSetting(settings.enableSubHighlight); this->rebuildListener_.addSetting(settings.enableSubHighlightSound); this->rebuildListener_.addSetting(settings.enableSubHighlightTaskbar); this->rebuildListener_.addSetting(settings.enableSelfMessageHighlight); this->rebuildListener_.addSetting( settings.showSelfMessageHighlightInMentions); // We do not need to rebuild the listener for the selfMessagesHighlightColor // The color is dynamically fetched any time the self message highlight is triggered this->rebuildListener_.addSetting(settings.subHighlightSoundUrl); 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_.addSetting(settings.enableAutomodHighlight); this->rebuildListener_.addSetting(settings.showAutomodInMentions); this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound); this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar); this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl); this->rebuildListener_.setCB([this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because a setting changed"; this->rebuildChecks(settings); }); this->signalHolder_.managedConnect( getSettings()->highlightedBadges.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight badges changed"; this->rebuildChecks(settings); }); this->signalHolder_.managedConnect( getSettings()->highlightedUsers.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight users changed"; this->rebuildChecks(settings); }); this->signalHolder_.managedConnect( getSettings()->highlightedMessages.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight messages changed"; this->rebuildChecks(settings); }); this->bConnections.emplace_back( accounts->twitch.currentUserChanged.connect([this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because user swapped accounts"; this->rebuildChecks(settings); })); this->rebuildChecks(settings); } void HighlightController::rebuildChecks(Settings &settings) { // Access checks for modification auto checks = this->checks_.access(); checks->clear(); // CURRENT ORDER: // Subscription -> Whisper -> Message -> User -> Reply Threads -> Badge rebuildSubscriptionHighlights(settings, *checks); rebuildWhisperHighlights(settings, *checks); rebuildMessageHighlights(settings, *checks); rebuildUserHighlights(settings, *checks); rebuildReplyThreadHighlight(settings, *checks); rebuildBadgeHighlights(settings, *checks); } std::pair HighlightController::check( const MessageParseArgs &args, const std::vector &badges, const QString &senderName, const QString &originalMessage, const MessageFlags &messageFlags) const { bool highlighted = false; auto result = HighlightResult::emptyResult(); // Access for checking const auto checks = this->checks_.accessConst(); auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); auto self = (senderName == currentUser->getUserName()); for (const auto &check : *checks) { if (auto checkResult = check.cb(args, badges, senderName, originalMessage, messageFlags, self); checkResult) { highlighted = true; if (checkResult->alert) { if (!result.alert) { result.alert = checkResult->alert; } } if (checkResult->playSound) { if (!result.playSound) { result.playSound = checkResult->playSound; } } if (checkResult->customSoundUrl) { if (!result.customSoundUrl) { result.customSoundUrl = checkResult->customSoundUrl; } } if (checkResult->color) { if (!result.color) { result.color = checkResult->color; } } if (checkResult->showInMentions) { if (!result.showInMentions) { result.showInMentions = checkResult->showInMentions; } } if (result.full()) { // The final highlight result does not have room to add any more parameters, early out break; } } } return {highlighted, result}; } } // namespace chatterino