diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c765cd7..544306830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922) +- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986) - Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) - Minor: The account switcher is now styled to match your theme. (#4817) - Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) diff --git a/src/Application.cpp b/src/Application.cpp index 0a2f8b4de..d8a7a5754 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -546,9 +546,15 @@ void Application::initPubSub() msg.senderUserID, msg.senderUserLogin, senderDisplayName, senderColor}; postToThread([chan, action] { - const auto p = makeAutomodMessage(action); + const auto p = + makeAutomodMessage(action, chan->getName()); chan->addMessage(p.first); chan->addMessage(p.second); + + getApp()->twitch->automodChannel->addMessage( + p.first); + getApp()->twitch->automodChannel->addMessage( + p.second); }); } // "ALLOWED" and "DENIED" statuses remain unimplemented @@ -573,7 +579,7 @@ void Application::initPubSub() } postToThread([chan, action] { - const auto p = makeAutomodMessage(action); + const auto p = makeAutomodMessage(action, chan->getName()); chan->addMessage(p.first); chan->addMessage(p.second); }); diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index f0dc5d185..47ae99c1c 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -295,7 +295,8 @@ bool Channel::isWritable() const { using Type = Channel::Type; auto type = this->getType(); - return type != Type::TwitchMentions && type != Type::TwitchLive; + return type != Type::TwitchMentions && type != Type::TwitchLive && + type != Type::TwitchAutomod; } void Channel::sendMessage(const QString &message) @@ -330,7 +331,8 @@ bool Channel::isLive() const bool Channel::shouldIgnoreHighlights() const { - return this->type_ == Type::TwitchMentions || + return this->type_ == Type::TwitchAutomod || + this->type_ == Type::TwitchMentions || this->type_ == Type::TwitchWhispers; } diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 7fcb5636c..4fc686c06 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -38,6 +38,7 @@ public: TwitchWatching, TwitchMentions, TwitchLive, + TwitchAutomod, TwitchEnd, Irc, Misc diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index 820916c36..0a7a72d16 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -30,7 +30,7 @@ namespace chatterino { enum class WindowType; struct SplitDescriptor { - // Twitch or mentions or watching or whispers or IRC + // Twitch or mentions or watching or live or automod or whispers or IRC QString type_; // Twitch Channel name or IRC channel name diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index c34fe74e5..b605544b9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -141,13 +141,14 @@ MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action) } std::pair makeAutomodMessage( - const AutomodAction &action) + const AutomodAction &action, const QString &channelName) { MessageBuilder builder, builder2; // // Builder for AutoMod message with explanation builder.message().loginName = "automod"; + builder.message().channelName = channelName; builder.message().flags.set(MessageFlag::PubSub); builder.message().flags.set(MessageFlag::Timeout); builder.message().flags.set(MessageFlag::AutoMod); @@ -193,6 +194,12 @@ std::pair makeAutomodMessage( // // Builder for offender's message + builder2.message().channelName = channelName; + builder2 + .emplace("#" + channelName, + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, channelName}); builder2.emplace(); builder2.emplace(); builder2.message().loginName = action.target.login; diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 28874439b..333d23469 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -54,7 +54,7 @@ const ImageUploaderResultTag imageUploaderResultMessage{}; MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); std::pair makeAutomodMessage( - const AutomodAction &action); + const AutomodAction &action, const QString &channelName); MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); struct MessageParseArgs { diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 3039f7358..5982b893d 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -43,6 +43,7 @@ TwitchIrcServer::TwitchIrcServer() : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) , liveChannel(new Channel("/live", Channel::Type::TwitchLive)) + , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) { this->initializeIrc(); @@ -272,6 +273,11 @@ std::shared_ptr TwitchIrcServer::getCustomChannel( return this->liveChannel; } + if (channelName == "/automod") + { + return this->automodChannel; + } + static auto getTimer = [](ChannelPtr channel, int msBetweenMessages, bool addInitialMessages) { if (addInitialMessages) @@ -383,6 +389,7 @@ void TwitchIrcServer::forEachChannelAndSpecialChannels( func(this->whispersChannel); func(this->mentionsChannel); func(this->liveChannel); + func(this->automodChannel); } std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index c3ff2436f..61fe8ddb3 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -77,6 +77,7 @@ public: const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; const ChannelPtr liveChannel; + const ChannelPtr automodChannel; IndirectChannel watchingChannel; PubSub *pubsub; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index be352588c..5174f2a33 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -603,6 +603,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) obj.insert("name", channel.get()->getName()); } break; + case Channel::Type::TwitchAutomod: { + obj.insert("type", "automod"); + } + break; case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); } @@ -676,6 +680,10 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { return app->twitch->liveChannel; } + else if (descriptor.type_ == "automod") + { + return app->twitch->automodChannel; + } else if (descriptor.type_ == "irc") { return Irc::instance().getOrAddChannel(descriptor.server_, diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index d73ec79e5..c6b36d11e 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -29,6 +29,10 @@ LoggingChannel::LoggingChannel(const QString &_channelName, { this->subDirectory = "Live"; } + else if (channelName.startsWith("/automod")) + { + this->subDirectory = "AutoMod"; + } else { this->subDirectory = @@ -96,7 +100,8 @@ void LoggingChannel::addMessage(MessagePtr message) } QString str; - if (channelName.startsWith("/mentions")) + if (channelName.startsWith("/mentions") || + channelName.startsWith("/automod")) { str.append("#" + message->channelName + " "); } diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index efc96daf8..2ac276e3e 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -1313,8 +1313,9 @@ SplitNotebook::SplitNotebook(Window *parent) { for (auto *split : sc->getSplits()) { - if (split->getChannel()->getType() != - Channel::Type::TwitchMentions) + auto type = split->getChannel()->getType(); + if (type != Channel::Type::TwitchMentions && + type != Channel::Type::TwitchAutomod) { if (split->getChannelView().scrollToMessage( message)) diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index c74bfcc2c..391c85457 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -140,10 +140,27 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) live_lbl->setVisible(enabled); }); + // automod_btn + auto automod_btn = vbox.emplace("AutoMod").assign( + &this->ui_.twitch.automod); + auto automod_lbl = + vbox.emplace("Shows when AutoMod catches a message in any " + "channel you moderate.") + .hidden(); + + automod_lbl->setWordWrap(true); + automod_btn->installEventFilter(&this->tabFilter_); + + QObject::connect(automod_btn.getElement(), &QRadioButton::toggled, + [=](bool enabled) mutable { + automod_lbl->setVisible(enabled); + }); + vbox->addStretch(1); // tabbing order - QWidget::setTabOrder(live_btn.getElement(), channel_btn.getElement()); + QWidget::setTabOrder(automod_btn.getElement(), + channel_btn.getElement()); QWidget::setTabOrder(channel_btn.getElement(), whispers_btn.getElement()); QWidget::setTabOrder(whispers_btn.getElement(), @@ -151,6 +168,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) QWidget::setTabOrder(mentions_btn.getElement(), watching_btn.getElement()); QWidget::setTabOrder(watching_btn.getElement(), live_btn.getElement()); + QWidget::setTabOrder(live_btn.getElement(), automod_btn.getElement()); // tab auto tab = notebook->addPage(obj.getElement()); @@ -311,6 +329,11 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) this->ui_.twitch.live->setFocus(); } break; + case Channel::Type::TwitchAutomod: { + this->ui_.notebook->selectIndex(TAB_TWITCH); + this->ui_.twitch.automod->setFocus(); + } + break; case Channel::Type::Irc: { this->ui_.notebook->selectIndex(TAB_IRC); this->ui_.irc.channel->setText(_channel.get()->getName()); @@ -378,6 +401,10 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const { return app->twitch->liveChannel; } + else if (this->ui_.twitch.automod->isChecked()) + { + return app->twitch->automodChannel; + } } break; case TAB_IRC: { @@ -442,9 +469,9 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched, this->dialog->ui_.twitch.whispers->setFocus(); return true; } - else if (widget == this->dialog->ui_.twitch.live) + else if (widget == this->dialog->ui_.twitch.automod) { - // Special case for when current selection is "Live" (the last entry in the list), next wrap is Channel, but we need to select its edit box + // Special case for when current selection is "AutoMod" (the last entry in the list), next wrap is Channel, but we need to select its edit box this->dialog->ui_.twitch.channel->setFocus(); return true; } @@ -463,7 +490,7 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched, if (widget == this->dialog->ui_.twitch.channelName) { // Special case for when current selection is the "Channel" entry's edit box since the Edit box actually has the focus - this->dialog->ui_.twitch.live->setFocus(); + this->dialog->ui_.twitch.automod->setFocus(); return true; } diff --git a/src/widgets/dialogs/SelectChannelDialog.hpp b/src/widgets/dialogs/SelectChannelDialog.hpp index bff084d18..65b2cb56b 100644 --- a/src/widgets/dialogs/SelectChannelDialog.hpp +++ b/src/widgets/dialogs/SelectChannelDialog.hpp @@ -49,6 +49,7 @@ private: QRadioButton *mentions; QRadioButton *watching; QRadioButton *live; + QRadioButton *automod; } twitch; struct { QLineEdit *channel; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c0f1f5212..2ac6848be 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1282,14 +1282,16 @@ MessageElementFlags ChannelView::getFlags() const flags.set(MessageElementFlag::ModeratorTools); } if (this->underlyingChannel_ == app->twitch->mentionsChannel || - this->underlyingChannel_ == app->twitch->liveChannel) + this->underlyingChannel_ == app->twitch->liveChannel || + this->underlyingChannel_ == app->twitch->automodChannel) { flags.set(MessageElementFlag::ChannelName); flags.unset(MessageElementFlag::ChannelPointReward); } } - if (this->sourceChannel_ == app->twitch->mentionsChannel) + if (this->sourceChannel_ == app->twitch->mentionsChannel || + this->sourceChannel_ == app->twitch->automodChannel) { flags.set(MessageElementFlag::ChannelName); } @@ -2347,11 +2349,13 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu, this->split_; bool isMentions = this->channel()->getType() == Channel::Type::TwitchMentions; - if (isSearch || isMentions || isReplyOrUserCard) + bool isAutomod = this->channel()->getType() == Channel::Type::TwitchAutomod; + if (isSearch || isMentions || isReplyOrUserCard || isAutomod) { const auto &messagePtr = layout->getMessagePtr(); menu->addAction("&Go to message", [this, &messagePtr, isSearch, - isMentions, isReplyOrUserCard] { + isMentions, isReplyOrUserCard, + isAutomod] { if (isSearch) { if (const auto &search = @@ -2360,16 +2364,17 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu, search->goToMessage(messagePtr); } } - else if (isMentions) + else if (isMentions || isAutomod) { getApp()->windows->scrollToMessage(messagePtr); } else if (isReplyOrUserCard) { - // If the thread is in the mentions channel, + // If the thread is in the mentions or automod channel, // we need to find the original split. - if (this->split_->getChannel()->getType() == - Channel::Type::TwitchMentions) + const auto type = this->split_->getChannel()->getType(); + if (type == Channel::Type::TwitchMentions || + type == Channel::Type::TwitchAutomod) { getApp()->windows->scrollToMessage(messagePtr); } @@ -2606,6 +2611,8 @@ bool ChannelView::mayContainMessage(const MessagePtr &message) return message->flags.has(MessageFlag::Highlighted); case Channel::Type::TwitchLive: return message->flags.has(MessageFlag::System); + case Channel::Type::TwitchAutomod: + return message->flags.has(MessageFlag::AutoMod); case Channel::Type::TwitchEnd: // TODO: not used? case Channel::Type::None: // Unspecific case Channel::Type::Misc: // Unspecific diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 329dff068..b34469308 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -134,7 +134,9 @@ void SearchPopup::goToMessage(const MessagePtr &message) { for (const auto &view : this->searchChannels_) { - if (view.get().channel()->getType() == Channel::Type::TwitchMentions) + const auto type = view.get().channel()->getType(); + if (type == Channel::Type::TwitchMentions || + type == Channel::Type::TwitchAutomod) { getApp()->windows->scrollToMessage(message); return; @@ -166,6 +168,10 @@ void SearchPopup::updateWindowTitle() { historyName = "multiple channels'"; } + else if (this->channelName_ == "/automod") + { + historyName = "automod"; + } else if (this->channelName_ == "/mentions") { historyName = "mentions"; diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 046b905ae..50d231fe7 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -1454,7 +1454,10 @@ void Split::showSearch(bool singleChannel) auto container = dynamic_cast(notebook.getPageAt(i)); for (auto split : container->getSplits()) { - popup->addChannel(split->getChannelView()); + if (split->channel_.getType() != Channel::Type::TwitchAutomod) + { + popup->addChannel(split->getChannelView()); + } } }