diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 5616a2633..43e88ee38 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,6 +3,7 @@ #include "Application.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" #include "singletons/Emotes.hpp" #include "singletons/Logging.hpp" #include "singletons/Settings.hpp" diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 938ee6428..3b5bb97a8 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -33,6 +33,7 @@ enum class MessageFlag : uint32_t { Whisper = (1 << 16), HighlightedWhisper = (1 << 17), Debug = (1 << 18), + Similar = (1 << 19), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 03097c3d1..c048bd626 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -141,6 +141,12 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) continue; } + if (getSettings()->hideSimilar && + this->message_->flags.has(MessageFlag::Similar)) + { + continue; + } + element->addToContainer(*this->container_, flags); } diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 1e3aae415..7a456ecaf 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -21,6 +21,97 @@ namespace chatterino { +static float relativeSimilarity(const QString &str1, const QString &str2) +{ + // Longest Common Substring Problem + std::vector> tree(str1.size(), + std::vector(str2.size(), 0)); + int z = 0; + + for (int i = 0; i < str1.size(); ++i) + { + for (int j = 0; j < str2.size(); ++j) + { + if (str1[i] == str2[j]) + { + if (i == 0 || j == 0) + { + tree[i][j] = 1; + } + else + { + tree[i][j] = tree[i - 1][j - 1] + 1; + } + if (tree[i][j] > z) + { + z = tree[i][j]; + } + } + else + { + tree[i][j] = 0; + } + } + } + + return z == 0 ? 0.f : float(z) / std::max(str1.size(), str2.size()); +}; + +float IrcMessageHandler::similarity( + MessagePtr msg, const LimitedQueueSnapshot &messages) +{ + float similarityPercent = 0.0f; + int bySameUser = 0; + for (int i = 1; bySameUser < getSettings()->hideSimilarMaxMessagesToCheck; + ++i) + { + if (messages.size() < i) + { + break; + } + const auto &prevMsg = messages[messages.size() - i]; + if (prevMsg->parseTime.secsTo(QTime::currentTime()) >= + getSettings()->hideSimilarMaxDelay) + { + break; + } + if (msg->loginName != prevMsg->loginName) + { + continue; + } + ++bySameUser; + similarityPercent = std::max( + similarityPercent, + relativeSimilarity(msg->messageText, prevMsg->messageText)); + } + return similarityPercent; +} + +void IrcMessageHandler::setSimilarityFlags(MessagePtr msg, ChannelPtr chan) +{ + if (getSettings()->similarityEnabled) + { + bool isMyself = msg->loginName == + getApp()->accounts->twitch.getCurrent()->getUserName(); + bool hideMyself = getSettings()->hideSimilarMyself; + + if (isMyself && !hideMyself) + { + return; + } + + if (IrcMessageHandler::similarity(msg, chan->getMessageSnapshot()) > + getSettings()->similarityPercentage) + { + msg->flags.set(MessageFlag::Similar, true); + if (getSettings()->colorSimilarDisabled) + { + msg->flags.set(MessageFlag::Disabled, true); + } + } + } +} + static QMap parseBadges(QString badgesString) { QMap badges; @@ -133,7 +224,16 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, } auto msg = builder.build(); - builder.triggerHighlights(); + + IrcMessageHandler::setSimilarityFlags(msg, chan); + + if (!msg->flags.has(MessageFlag::Similar) || + (!getSettings()->hideSimilar && + getSettings()->shownSimilarTriggerHighlights)) + { + builder.triggerHighlights(); + } + auto highlighted = msg->flags.has(MessageFlag::Highlighted); if (!isSub) diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index fe8321adf..9228054d8 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include "common/Channel.hpp" #include "messages/Message.hpp" namespace chatterino { @@ -49,6 +50,10 @@ public: void handleJoinMessage(Communi::IrcMessage *message); void handlePartMessage(Communi::IrcMessage *message); + static float similarity(MessagePtr msg, + const LimitedQueueSnapshot &messages); + static void setSimilarityFlags(MessagePtr message, ChannelPtr channel); + private: void addMessage(Communi::IrcMessage *message, const QString &target, const QString &content, TwitchIrcServer &server, diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 050c9db98..f0d1679d4 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -249,6 +249,20 @@ public: IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0}; IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0}; + // Similarity + BoolSetting similarityEnabled = {"/similarity/similarityEnabled", false}; + BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled", + true}; + BoolSetting hideSimilar = {"/similarity/hideSimilar", false}; + BoolSetting hideSimilarMyself = {"/similarity/hideSimilarMyself", false}; + BoolSetting shownSimilarTriggerHighlights = { + "/similarity/shownSimilarTriggerHighlights", false}; + FloatSetting similarityPercentage = {"/similarity/similarityPercentage", + 0.9f}; + IntSetting hideSimilarMaxDelay = {"/similarity/hideSimilarMaxDelay", 5}; + IntSetting hideSimilarMaxMessagesToCheck = { + "/similarity/hideSimilarMaxMessagesToCheck", 3}; + private: void updateModerationActions(); }; diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 2824a8b38..c291be6c2 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -351,6 +351,11 @@ void Window::addShortcuts() getApp()->twitch.server->getOrAddChannel(si.channelName)); splitContainer->appendSplit(split); }); + + createWindowShortcut(this, "CTRL+H", [this] { + getSettings()->hideSimilar.setValue(!getSettings()->hideSimilar); + getApp()->windows->forceLayoutChannelViews(); + }); } void Window::addMenuBar() diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 0ffa1c8b5..4032db9e2 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -547,6 +547,33 @@ void GeneralPage::initLayout(SettingsLayout &layout) QDesktopServices::openUrl(getPaths()->rootAppDataDirectory); }); + layout.addTitle("Similarity"); + layout.addDescription( + "Hides or grays out similar messages from the same user"); + layout.addCheckbox("Similarity enabled", s.similarityEnabled); + layout.addCheckbox("Gray out similar messages", s.colorSimilarDisabled); + layout.addCheckbox("Hide similar messages (Ctrl + H)", s.hideSimilar); + layout.addCheckbox("Hide or gray out my own similar messages", + s.hideSimilarMyself); + layout.addCheckbox("Shown similar messages trigger highlights", + s.shownSimilarTriggerHighlights); + layout.addDropdown( + "Similarity percentage", {"0.5", "0.75", "0.9"}, s.similarityPercentage, + [](auto val) { return QString::number(val); }, + [](auto args) { return fuzzyToFloat(args.value, 0.9f); }); + s.hideSimilar.connect( + []() { getApp()->windows->forceLayoutChannelViews(); }, false); + layout.addDropdown( + "Similar messages max delay in seconds", + {"5", "10", "15", "30", "60", "120"}, s.hideSimilarMaxDelay, + [](auto val) { return QString::number(val); }, + [](auto args) { return fuzzyToInt(args.value, 5); }); + layout.addDropdown( + "Similar messages max previous messages to check", + {"1", "2", "3", "4", "5"}, s.hideSimilarMaxMessagesToCheck, + [](auto val) { return QString::number(val); }, + [](auto args) { return fuzzyToInt(args.value, 3); }); + // invisible element for width auto inv = new BaseWidget(this); inv->setScaleIndependantWidth(500); diff --git a/src/widgets/settingspages/KeyboardSettingsPage.cpp b/src/widgets/settingspages/KeyboardSettingsPage.cpp index 6b05cf945..2d2a809fd 100644 --- a/src/widgets/settingspages/KeyboardSettingsPage.cpp +++ b/src/widgets/settingspages/KeyboardSettingsPage.cpp @@ -26,6 +26,10 @@ KeyboardSettingsPage::KeyboardSettingsPage() form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab")); form->addRow(new QLabel("Ctrl + Shift + W"), new QLabel("Close current tab")); + form->addRow( + new QLabel("Ctrl + H"), + new QLabel( + "Hide/Show similar messages (Enable in General under Similarity)")); form->addItem(new QSpacerItem(16, 16)); form->addRow(new QLabel("Ctrl + 1/2/3/..."),