feat: add sound and flash alert for automod caught messages (#5026)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
|
- 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)
|
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
|
||||||
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
|
- 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: 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)
|
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
|
||||||
|
|||||||
+6
-3
@@ -552,7 +552,8 @@ void Application::initPubSub()
|
|||||||
senderDisplayName, senderColor};
|
senderDisplayName, senderColor};
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p =
|
const auto p =
|
||||||
makeAutomodMessage(action, chan->getName());
|
TwitchMessageBuilder::makeAutomodMessage(
|
||||||
|
action, chan->getName());
|
||||||
chan->addMessage(p.first);
|
chan->addMessage(p.first);
|
||||||
chan->addMessage(p.second);
|
chan->addMessage(p.second);
|
||||||
|
|
||||||
@@ -584,7 +585,8 @@ void Application::initPubSub()
|
|||||||
}
|
}
|
||||||
|
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p = makeAutomodMessage(action, chan->getName());
|
const auto p = TwitchMessageBuilder::makeAutomodMessage(
|
||||||
|
action, chan->getName());
|
||||||
chan->addMessage(p.first);
|
chan->addMessage(p.first);
|
||||||
chan->addMessage(p.second);
|
chan->addMessage(p.second);
|
||||||
});
|
});
|
||||||
@@ -626,7 +628,8 @@ void Application::initPubSub()
|
|||||||
}
|
}
|
||||||
|
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p = makeAutomodInfoMessage(action);
|
const auto p =
|
||||||
|
TwitchMessageBuilder::makeAutomodInfoMessage(action);
|
||||||
chan->addMessage(p);
|
chan->addMessage(p);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -204,6 +204,41 @@ void rebuildMessageHighlights(Settings &settings,
|
|||||||
{
|
{
|
||||||
checks.emplace_back(highlightPhraseCheck(highlight));
|
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();
|
||||||
|
|
||||||
|
checks.emplace_back(HighlightCheck{
|
||||||
|
[=](const auto & /*args*/, const auto & /*badges*/,
|
||||||
|
const auto & /*senderName*/, const auto & /*originalMessage*/,
|
||||||
|
const auto &flags,
|
||||||
|
const auto /*self*/) -> std::optional<HighlightResult> {
|
||||||
|
if (!flags.has(MessageFlag::AutoModOffendingMessage))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QUrl> highlightSoundUrl;
|
||||||
|
if (!highlightSoundUrlValue.isEmpty())
|
||||||
|
{
|
||||||
|
highlightSoundUrl = highlightSoundUrlValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HighlightResult{
|
||||||
|
highlightAlert, // alert
|
||||||
|
highlightSound, // playSound
|
||||||
|
highlightSoundUrl, // customSoundUrl
|
||||||
|
nullptr, // color
|
||||||
|
false, // showInMentions
|
||||||
|
};
|
||||||
|
}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void rebuildUserHighlights(Settings &settings,
|
void rebuildUserHighlights(Settings &settings,
|
||||||
@@ -434,6 +469,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
|
|||||||
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
|
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
|
||||||
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
|
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
|
||||||
|
|
||||||
|
this->rebuildListener_.addSetting(settings.enableAutomodHighlight);
|
||||||
|
this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound);
|
||||||
|
this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar);
|
||||||
|
this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl);
|
||||||
|
|
||||||
this->rebuildListener_.setCB([this, &settings] {
|
this->rebuildListener_.setCB([this, &settings] {
|
||||||
qCDebug(chatterinoHighlights)
|
qCDebug(chatterinoHighlights)
|
||||||
<< "Rebuild checks because a setting changed";
|
<< "Rebuild checks because a setting changed";
|
||||||
|
|||||||
@@ -234,6 +234,30 @@ void HighlightModel::afterInit()
|
|||||||
|
|
||||||
this->insertCustomRow(threadMessageRow,
|
this->insertCustomRow(threadMessageRow,
|
||||||
HighlightRowIndexes::ThreadMessageRow);
|
HighlightRowIndexes::ThreadMessageRow);
|
||||||
|
|
||||||
|
// Highlight settings for automod caught messages
|
||||||
|
const std::vector<QStandardItem *> automodRow = this->createRow();
|
||||||
|
setBoolItem(automodRow[Column::Pattern],
|
||||||
|
getSettings()->enableAutomodHighlight.getValue(), true, false);
|
||||||
|
automodRow[Column::Pattern]->setData("AutoMod Caught Messages",
|
||||||
|
Qt::DisplayRole);
|
||||||
|
automodRow[Column::ShowInMentions]->setFlags({});
|
||||||
|
setBoolItem(automodRow[Column::FlashTaskbar],
|
||||||
|
getSettings()->enableAutomodHighlightTaskbar.getValue(), true,
|
||||||
|
false);
|
||||||
|
setBoolItem(automodRow[Column::PlaySound],
|
||||||
|
getSettings()->enableAutomodHighlightSound.getValue(), true,
|
||||||
|
false);
|
||||||
|
automodRow[Column::UseRegex]->setFlags({});
|
||||||
|
automodRow[Column::CaseSensitive]->setFlags({});
|
||||||
|
|
||||||
|
const auto automodSound =
|
||||||
|
QUrl(getSettings()->automodHighlightSoundUrl.getValue());
|
||||||
|
setFilePathItem(automodRow[Column::SoundPath], automodSound, false);
|
||||||
|
|
||||||
|
automodRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
|
||||||
|
|
||||||
|
this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
@@ -278,6 +302,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||||||
getSettings()->enableThreadHighlight.setValue(
|
getSettings()->enableThreadHighlight.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlight.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -336,6 +365,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||||||
getSettings()->enableThreadHighlightTaskbar.setValue(
|
getSettings()->enableThreadHighlightTaskbar.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlightTaskbar.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -377,6 +411,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||||||
getSettings()->enableThreadHighlightSound.setValue(
|
getSettings()->enableThreadHighlightSound.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlightSound.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -412,6 +451,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||||||
getSettings()->threadHighlightSoundUrl.setValue(
|
getSettings()->threadHighlightSoundUrl.setValue(
|
||||||
value.toString());
|
value.toString());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->automodHighlightSoundUrl.setValue(
|
||||||
|
value.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public:
|
|||||||
FirstMessageRow = 4,
|
FirstMessageRow = 4,
|
||||||
ElevatedMessageRow = 5,
|
ElevatedMessageRow = 5,
|
||||||
ThreadMessageRow = 6,
|
ThreadMessageRow = 6,
|
||||||
|
AutomodRow = 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum UserHighlightRowIndexes {
|
enum UserHighlightRowIndexes {
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ enum class MessageFlag : int64_t {
|
|||||||
LiveUpdatesAdd = (1LL << 28),
|
LiveUpdatesAdd = (1LL << 28),
|
||||||
LiveUpdatesRemove = (1LL << 29),
|
LiveUpdatesRemove = (1LL << 29),
|
||||||
LiveUpdatesUpdate = (1LL << 30),
|
LiveUpdatesUpdate = (1LL << 30),
|
||||||
|
/// The message caught by AutoMod containing the user who sent the message & its contents
|
||||||
|
AutoModOffendingMessage = (1LL << 31),
|
||||||
};
|
};
|
||||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||||
|
|
||||||
|
|||||||
@@ -78,159 +78,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time)
|
|||||||
return MessageBuilder(systemMessage, text, time).release();
|
return MessageBuilder(systemMessage, text, time).release();
|
||||||
}
|
}
|
||||||
|
|
||||||
EmotePtr makeAutoModBadge()
|
|
||||||
{
|
|
||||||
return std::make_shared<Emote>(Emote{
|
|
||||||
EmoteName{},
|
|
||||||
ImageSet{Image::fromResourcePixmap(getResources().twitch.automod)},
|
|
||||||
Tooltip{"AutoMod"},
|
|
||||||
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
|
|
||||||
}
|
|
||||||
|
|
||||||
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action)
|
|
||||||
{
|
|
||||||
auto builder = MessageBuilder();
|
|
||||||
QString text("AutoMod: ");
|
|
||||||
|
|
||||||
builder.emplace<TimestampElement>();
|
|
||||||
builder.message().flags.set(MessageFlag::PubSub);
|
|
||||||
|
|
||||||
// AutoMod shield badge
|
|
||||||
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
|
||||||
MessageElementFlag::BadgeChannelAuthority);
|
|
||||||
// AutoMod "username"
|
|
||||||
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
|
||||||
MessageColor(QColor("blue")),
|
|
||||||
FontStyle::ChatMediumBold);
|
|
||||||
builder.emplace<TextElement>(
|
|
||||||
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
|
||||||
MessageColor(QColor("blue")));
|
|
||||||
switch (action.type)
|
|
||||||
{
|
|
||||||
case AutomodInfoAction::OnHold: {
|
|
||||||
QString info("Hey! Your message is being checked "
|
|
||||||
"by mods and has not been sent.");
|
|
||||||
text += info;
|
|
||||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
|
||||||
MessageColor::Text);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case AutomodInfoAction::Denied: {
|
|
||||||
QString info("Mods have removed your message.");
|
|
||||||
text += info;
|
|
||||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
|
||||||
MessageColor::Text);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case AutomodInfoAction::Approved: {
|
|
||||||
QString info("Mods have accepted your message.");
|
|
||||||
text += info;
|
|
||||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
|
||||||
MessageColor::Text);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.message().flags.set(MessageFlag::AutoMod);
|
|
||||||
builder.message().messageText = text;
|
|
||||||
builder.message().searchText = text;
|
|
||||||
|
|
||||||
auto message = builder.release();
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|
||||||
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);
|
|
||||||
|
|
||||||
// AutoMod shield badge
|
|
||||||
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
|
||||||
MessageElementFlag::BadgeChannelAuthority);
|
|
||||||
// AutoMod "username"
|
|
||||||
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
|
||||||
MessageColor(QColor("blue")),
|
|
||||||
FontStyle::ChatMediumBold);
|
|
||||||
builder.emplace<TextElement>(
|
|
||||||
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
|
||||||
MessageColor(QColor("blue")));
|
|
||||||
// AutoMod header message
|
|
||||||
builder.emplace<TextElement>(
|
|
||||||
("Held a message for reason: " + action.reason +
|
|
||||||
". Allow will post it in chat. "),
|
|
||||||
MessageElementFlag::Text, MessageColor::Text);
|
|
||||||
// Allow link button
|
|
||||||
builder
|
|
||||||
.emplace<TextElement>("Allow", MessageElementFlag::Text,
|
|
||||||
MessageColor(QColor("green")),
|
|
||||||
FontStyle::ChatMediumBold)
|
|
||||||
->setLink({Link::AutoModAllow, action.msgID});
|
|
||||||
// Deny link button
|
|
||||||
builder
|
|
||||||
.emplace<TextElement>(" Deny", MessageElementFlag::Text,
|
|
||||||
MessageColor(QColor("red")),
|
|
||||||
FontStyle::ChatMediumBold)
|
|
||||||
->setLink({Link::AutoModDeny, action.msgID});
|
|
||||||
// ID of message caught by AutoMod
|
|
||||||
// builder.emplace<TextElement>(action.msgID, MessageElementFlag::Text,
|
|
||||||
// MessageColor::Text);
|
|
||||||
auto text1 =
|
|
||||||
QString("AutoMod: Held a message for reason: %1. Allow will post "
|
|
||||||
"it in chat. Allow Deny")
|
|
||||||
.arg(action.reason);
|
|
||||||
builder.message().messageText = text1;
|
|
||||||
builder.message().searchText = text1;
|
|
||||||
|
|
||||||
auto message1 = builder.release();
|
|
||||||
|
|
||||||
//
|
|
||||||
// Builder for offender's message
|
|
||||||
builder2.message().channelName = channelName;
|
|
||||||
builder2
|
|
||||||
.emplace<TextElement>("#" + channelName,
|
|
||||||
MessageElementFlag::ChannelName,
|
|
||||||
MessageColor::System)
|
|
||||||
->setLink({Link::JumpToChannel, channelName});
|
|
||||||
builder2.emplace<TimestampElement>();
|
|
||||||
builder2.emplace<TwitchModerationElement>();
|
|
||||||
builder2.message().loginName = action.target.login;
|
|
||||||
builder2.message().flags.set(MessageFlag::PubSub);
|
|
||||||
builder2.message().flags.set(MessageFlag::Timeout);
|
|
||||||
builder2.message().flags.set(MessageFlag::AutoMod);
|
|
||||||
|
|
||||||
// sender username
|
|
||||||
builder2
|
|
||||||
.emplace<TextElement>(
|
|
||||||
action.target.displayName + ":", MessageElementFlag::BoldUsername,
|
|
||||||
MessageColor(action.target.color), FontStyle::ChatMediumBold)
|
|
||||||
->setLink({Link::UserInfo, action.target.login});
|
|
||||||
builder2
|
|
||||||
.emplace<TextElement>(action.target.displayName + ":",
|
|
||||||
MessageElementFlag::NonBoldUsername,
|
|
||||||
MessageColor(action.target.color))
|
|
||||||
->setLink({Link::UserInfo, action.target.login});
|
|
||||||
// sender's message caught by AutoMod
|
|
||||||
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
|
|
||||||
MessageColor::Text);
|
|
||||||
auto text2 =
|
|
||||||
QString("%1: %2").arg(action.target.displayName, action.message);
|
|
||||||
builder2.message().messageText = text2;
|
|
||||||
builder2.message().searchText = text2;
|
|
||||||
|
|
||||||
auto message2 = builder2.release();
|
|
||||||
|
|
||||||
return std::make_pair(message1, message2);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBuilder::MessageBuilder()
|
MessageBuilder::MessageBuilder()
|
||||||
: message_(std::make_shared<Message>())
|
: message_(std::make_shared<Message>())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ const ImageUploaderResultTag imageUploaderResultMessage{};
|
|||||||
|
|
||||||
MessagePtr makeSystemMessage(const QString &text);
|
MessagePtr makeSystemMessage(const QString &text);
|
||||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|
||||||
const AutomodAction &action, const QString &channelName);
|
|
||||||
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
|
|
||||||
|
|
||||||
struct MessageParseArgs {
|
struct MessageParseArgs {
|
||||||
bool disablePingSounds = false;
|
bool disablePingSounds = false;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
@@ -170,18 +172,10 @@ void SharedMessageBuilder::parseHighlights()
|
|||||||
this->highlightAlert_ = highlightResult.alert;
|
this->highlightAlert_ = highlightResult.alert;
|
||||||
|
|
||||||
this->highlightSound_ = highlightResult.playSound;
|
this->highlightSound_ = highlightResult.playSound;
|
||||||
|
this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl;
|
||||||
|
|
||||||
this->message().highlightColor = highlightResult.color;
|
this->message().highlightColor = highlightResult.color;
|
||||||
|
|
||||||
if (highlightResult.customSoundUrl)
|
|
||||||
{
|
|
||||||
this->highlightSoundUrl_ = *highlightResult.customSoundUrl;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highlightResult.showInMentions)
|
if (highlightResult.showInMentions)
|
||||||
{
|
{
|
||||||
this->message().flags.set(MessageFlag::ShowInMentions);
|
this->message().flags.set(MessageFlag::ShowInMentions);
|
||||||
@@ -199,6 +193,15 @@ void SharedMessageBuilder::appendChannelName()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SharedMessageBuilder::triggerHighlights()
|
void SharedMessageBuilder::triggerHighlights()
|
||||||
|
{
|
||||||
|
SharedMessageBuilder::triggerHighlights(
|
||||||
|
this->channel->getName(), this->highlightSound_,
|
||||||
|
this->highlightSoundCustomUrl_, this->highlightAlert_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SharedMessageBuilder::triggerHighlights(
|
||||||
|
const QString &channelName, bool playSound,
|
||||||
|
const std::optional<QUrl> &customSoundUrl, bool windowAlert)
|
||||||
{
|
{
|
||||||
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
||||||
{
|
{
|
||||||
@@ -206,21 +209,32 @@ void SharedMessageBuilder::triggerHighlights()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSettings()->isMutedChannel(this->channel->getName()))
|
if (getSettings()->isMutedChannel(channelName))
|
||||||
{
|
{
|
||||||
// Do nothing. Pings are muted in this channel.
|
// Do nothing. Pings are muted in this channel.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
const bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
const bool resolveFocus =
|
||||||
|
!hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||||
|
|
||||||
if (this->highlightSound_ && resolveFocus)
|
if (playSound && resolveFocus)
|
||||||
{
|
{
|
||||||
getIApp()->getSound()->play(this->highlightSoundUrl_);
|
// TODO(C++23): optional or_else
|
||||||
|
QUrl soundUrl;
|
||||||
|
if (customSoundUrl)
|
||||||
|
{
|
||||||
|
soundUrl = *customSoundUrl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
soundUrl = getFallbackHighlightSound();
|
||||||
|
}
|
||||||
|
getIApp()->getSound()->play(soundUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->highlightAlert_)
|
if (windowAlert)
|
||||||
{
|
{
|
||||||
getApp()->windows->sendAlert();
|
getApp()->windows->sendAlert();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
class Badge;
|
class Badge;
|
||||||
@@ -57,6 +59,9 @@ protected:
|
|||||||
|
|
||||||
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
// 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 parseHighlights();
|
||||||
|
static void triggerHighlights(const QString &channelName, bool playSound,
|
||||||
|
const std::optional<QUrl> &customSoundUrl,
|
||||||
|
bool windowAlert);
|
||||||
|
|
||||||
void appendChannelName();
|
void appendChannelName();
|
||||||
|
|
||||||
@@ -72,8 +77,7 @@ protected:
|
|||||||
|
|
||||||
bool highlightAlert_ = false;
|
bool highlightAlert_ = false;
|
||||||
bool highlightSound_ = false;
|
bool highlightSound_ = false;
|
||||||
|
std::optional<QUrl> highlightSoundCustomUrl_{};
|
||||||
QUrl highlightSoundUrl_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -342,9 +342,13 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
|||||||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
||||||
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
||||||
{
|
{
|
||||||
// Blend highlight color with usual background color
|
assert(this->message_->highlightColor);
|
||||||
backgroundColor =
|
if (this->message_->highlightColor)
|
||||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
{
|
||||||
|
// Blend highlight color with usual background color
|
||||||
|
backgroundColor =
|
||||||
|
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
||||||
ctx.preferences.enableSubHighlight)
|
ctx.preferences.enableSubHighlight)
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ void MessagePreferences::connectSettings(Settings *settings,
|
|||||||
},
|
},
|
||||||
holder);
|
holder);
|
||||||
|
|
||||||
|
settings->enableAutomodHighlight.connect(
|
||||||
|
[this](const auto &newValue) {
|
||||||
|
this->enableAutomodHighlight = newValue;
|
||||||
|
},
|
||||||
|
holder);
|
||||||
|
|
||||||
settings->alternateMessages.connect(
|
settings->alternateMessages.connect(
|
||||||
[this](const auto &newValue) {
|
[this](const auto &newValue) {
|
||||||
this->alternateMessages = newValue;
|
this->alternateMessages = newValue;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ struct MessagePreferences {
|
|||||||
bool enableElevatedMessageHighlight{};
|
bool enableElevatedMessageHighlight{};
|
||||||
bool enableFirstMessageHighlight{};
|
bool enableFirstMessageHighlight{};
|
||||||
bool enableSubHighlight{};
|
bool enableSubHighlight{};
|
||||||
|
bool enableAutomodHighlight{};
|
||||||
|
|
||||||
bool alternateMessages{};
|
bool alternateMessages{};
|
||||||
bool separateMessages{};
|
bool separateMessages{};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "common/Literals.hpp"
|
#include "common/Literals.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "controllers/highlights/HighlightController.hpp"
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||||
#include "controllers/userdata/UserDataController.hpp"
|
#include "controllers/userdata/UserDataController.hpp"
|
||||||
@@ -1765,6 +1766,173 @@ MessagePtr TwitchMessageBuilder::buildHypeChatMessage(
|
|||||||
return builder.release();
|
return builder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmotePtr makeAutoModBadge()
|
||||||
|
{
|
||||||
|
return std::make_shared<Emote>(Emote{
|
||||||
|
EmoteName{},
|
||||||
|
ImageSet{Image::fromResourcePixmap(getResources().twitch.automod)},
|
||||||
|
Tooltip{"AutoMod"},
|
||||||
|
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagePtr TwitchMessageBuilder::makeAutomodInfoMessage(
|
||||||
|
const AutomodInfoAction &action)
|
||||||
|
{
|
||||||
|
auto builder = MessageBuilder();
|
||||||
|
QString text("AutoMod: ");
|
||||||
|
|
||||||
|
builder.emplace<TimestampElement>();
|
||||||
|
builder.message().flags.set(MessageFlag::PubSub);
|
||||||
|
|
||||||
|
// AutoMod shield badge
|
||||||
|
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
||||||
|
MessageElementFlag::BadgeChannelAuthority);
|
||||||
|
// AutoMod "username"
|
||||||
|
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
||||||
|
MessageColor(QColor("blue")),
|
||||||
|
FontStyle::ChatMediumBold);
|
||||||
|
builder.emplace<TextElement>(
|
||||||
|
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
||||||
|
MessageColor(QColor("blue")));
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case AutomodInfoAction::OnHold: {
|
||||||
|
QString info("Hey! Your message is being checked "
|
||||||
|
"by mods and has not been sent.");
|
||||||
|
text += info;
|
||||||
|
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||||
|
MessageColor::Text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AutomodInfoAction::Denied: {
|
||||||
|
QString info("Mods have removed your message.");
|
||||||
|
text += info;
|
||||||
|
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||||
|
MessageColor::Text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AutomodInfoAction::Approved: {
|
||||||
|
QString info("Mods have accepted your message.");
|
||||||
|
text += info;
|
||||||
|
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||||
|
MessageColor::Text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.message().flags.set(MessageFlag::AutoMod);
|
||||||
|
builder.message().messageText = text;
|
||||||
|
builder.message().searchText = text;
|
||||||
|
|
||||||
|
auto message = builder.release();
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeAutomodMessage(
|
||||||
|
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);
|
||||||
|
|
||||||
|
// AutoMod shield badge
|
||||||
|
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
||||||
|
MessageElementFlag::BadgeChannelAuthority);
|
||||||
|
// AutoMod "username"
|
||||||
|
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
||||||
|
MessageColor(QColor("blue")),
|
||||||
|
FontStyle::ChatMediumBold);
|
||||||
|
builder.emplace<TextElement>(
|
||||||
|
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
||||||
|
MessageColor(QColor("blue")));
|
||||||
|
// AutoMod header message
|
||||||
|
builder.emplace<TextElement>(
|
||||||
|
("Held a message for reason: " + action.reason +
|
||||||
|
". Allow will post it in chat. "),
|
||||||
|
MessageElementFlag::Text, MessageColor::Text);
|
||||||
|
// Allow link button
|
||||||
|
builder
|
||||||
|
.emplace<TextElement>("Allow", MessageElementFlag::Text,
|
||||||
|
MessageColor(QColor("green")),
|
||||||
|
FontStyle::ChatMediumBold)
|
||||||
|
->setLink({Link::AutoModAllow, action.msgID});
|
||||||
|
// Deny link button
|
||||||
|
builder
|
||||||
|
.emplace<TextElement>(" Deny", MessageElementFlag::Text,
|
||||||
|
MessageColor(QColor("red")),
|
||||||
|
FontStyle::ChatMediumBold)
|
||||||
|
->setLink({Link::AutoModDeny, action.msgID});
|
||||||
|
// ID of message caught by AutoMod
|
||||||
|
// builder.emplace<TextElement>(action.msgID, MessageElementFlag::Text,
|
||||||
|
// MessageColor::Text);
|
||||||
|
auto text1 =
|
||||||
|
QString("AutoMod: Held a message for reason: %1. Allow will post "
|
||||||
|
"it in chat. Allow Deny")
|
||||||
|
.arg(action.reason);
|
||||||
|
builder.message().messageText = text1;
|
||||||
|
builder.message().searchText = text1;
|
||||||
|
|
||||||
|
auto message1 = builder.release();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Builder for offender's message
|
||||||
|
builder2.message().channelName = channelName;
|
||||||
|
builder2
|
||||||
|
.emplace<TextElement>("#" + channelName,
|
||||||
|
MessageElementFlag::ChannelName,
|
||||||
|
MessageColor::System)
|
||||||
|
->setLink({Link::JumpToChannel, channelName});
|
||||||
|
builder2.emplace<TimestampElement>();
|
||||||
|
builder2.emplace<TwitchModerationElement>();
|
||||||
|
builder2.message().loginName = action.target.login;
|
||||||
|
builder2.message().flags.set(MessageFlag::PubSub);
|
||||||
|
builder2.message().flags.set(MessageFlag::Timeout);
|
||||||
|
builder2.message().flags.set(MessageFlag::AutoMod);
|
||||||
|
builder2.message().flags.set(MessageFlag::AutoModOffendingMessage);
|
||||||
|
|
||||||
|
// sender username
|
||||||
|
builder2
|
||||||
|
.emplace<TextElement>(
|
||||||
|
action.target.displayName + ":", MessageElementFlag::BoldUsername,
|
||||||
|
MessageColor(action.target.color), FontStyle::ChatMediumBold)
|
||||||
|
->setLink({Link::UserInfo, action.target.login});
|
||||||
|
builder2
|
||||||
|
.emplace<TextElement>(action.target.displayName + ":",
|
||||||
|
MessageElementFlag::NonBoldUsername,
|
||||||
|
MessageColor(action.target.color))
|
||||||
|
->setLink({Link::UserInfo, action.target.login});
|
||||||
|
// sender's message caught by AutoMod
|
||||||
|
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
|
||||||
|
MessageColor::Text);
|
||||||
|
auto text2 =
|
||||||
|
QString("%1: %2").arg(action.target.displayName, action.message);
|
||||||
|
builder2.message().messageText = text2;
|
||||||
|
builder2.message().searchText = text2;
|
||||||
|
|
||||||
|
auto message2 = builder2.release();
|
||||||
|
|
||||||
|
// Normally highlights would be checked & triggered during the builder parse steps
|
||||||
|
// and when the message is added to the channel
|
||||||
|
// We do this a bit weird since the message comes in from PubSub and not the normal message route
|
||||||
|
auto [highlighted, highlightResult] = getIApp()->getHighlights()->check(
|
||||||
|
{}, {}, action.target.login, action.message, message2->flags);
|
||||||
|
if (highlighted)
|
||||||
|
{
|
||||||
|
SharedMessageBuilder::triggerHighlights(
|
||||||
|
channelName, highlightResult.playSound,
|
||||||
|
highlightResult.customSoundUrl, highlightResult.alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_pair(message1, message2);
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
|
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
|
||||||
{
|
{
|
||||||
this->thread_ = std::move(thread);
|
this->thread_ = std::move(thread);
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ public:
|
|||||||
|
|
||||||
static MessagePtr buildHypeChatMessage(Communi::IrcPrivateMessage *message);
|
static MessagePtr buildHypeChatMessage(Communi::IrcPrivateMessage *message);
|
||||||
|
|
||||||
|
static std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
||||||
|
const AutomodAction &action, const QString &channelName);
|
||||||
|
static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
|
||||||
|
|
||||||
// Shares some common logic from SharedMessageBuilder::parseBadgeTag
|
// Shares some common logic from SharedMessageBuilder::parseBadgeTag
|
||||||
static std::unordered_map<QString, QString> parseBadgeInfoTag(
|
static std::unordered_map<QString, QString> parseBadgeInfoTag(
|
||||||
const QVariantMap &tags);
|
const QVariantMap &tags);
|
||||||
|
|||||||
@@ -375,6 +375,23 @@ public:
|
|||||||
""};
|
""};
|
||||||
QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""};
|
QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""};
|
||||||
|
|
||||||
|
BoolSetting enableAutomodHighlight = {
|
||||||
|
"/highlighting/automod/enabled",
|
||||||
|
true,
|
||||||
|
};
|
||||||
|
BoolSetting enableAutomodHighlightSound = {
|
||||||
|
"/highlighting/automod/enableSound",
|
||||||
|
false,
|
||||||
|
};
|
||||||
|
BoolSetting enableAutomodHighlightTaskbar = {
|
||||||
|
"/highlighting/automod/enableTaskbarFlashing",
|
||||||
|
false,
|
||||||
|
};
|
||||||
|
QStringSetting automodHighlightSoundUrl = {
|
||||||
|
"/highlighting/automod/soundUrl",
|
||||||
|
"",
|
||||||
|
};
|
||||||
|
|
||||||
BoolSetting enableThreadHighlight = {
|
BoolSetting enableThreadHighlight = {
|
||||||
"/highlighting/thread/nameIsHighlightKeyword", true};
|
"/highlighting/thread/nameIsHighlightKeyword", true};
|
||||||
BoolSetting showThreadHighlightInMentions = {
|
BoolSetting showThreadHighlightInMentions = {
|
||||||
|
|||||||
@@ -1093,12 +1093,13 @@ void ChannelView::messageAppended(MessagePtr &message,
|
|||||||
|
|
||||||
if (!messageFlags->has(MessageFlag::DoNotTriggerNotification))
|
if (!messageFlags->has(MessageFlag::DoNotTriggerNotification))
|
||||||
{
|
{
|
||||||
if (messageFlags->has(MessageFlag::Highlighted) &&
|
if ((messageFlags->has(MessageFlag::Highlighted) &&
|
||||||
messageFlags->has(MessageFlag::ShowInMentions) &&
|
messageFlags->has(MessageFlag::ShowInMentions) &&
|
||||||
!messageFlags->has(MessageFlag::Subscription) &&
|
!messageFlags->has(MessageFlag::Subscription) &&
|
||||||
(getSettings()->highlightMentions ||
|
(getSettings()->highlightMentions ||
|
||||||
this->channel_->getType() != Channel::Type::TwitchMentions))
|
this->channel_->getType() != Channel::Type::TwitchMentions)) ||
|
||||||
|
(this->channel_->getType() == Channel::Type::TwitchAutomod &&
|
||||||
|
getSettings()->enableAutomodHighlight))
|
||||||
{
|
{
|
||||||
this->tabHighlightRequested.invoke(HighlightState::Highlighted);
|
this->tabHighlightRequested.invoke(HighlightState::Highlighted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ ScrollbarHighlight::ScrollbarHighlight(const std::shared_ptr<QColor> color,
|
|||||||
|
|
||||||
QColor ScrollbarHighlight::getColor() const
|
QColor ScrollbarHighlight::getColor() const
|
||||||
{
|
{
|
||||||
|
assert(this->color_);
|
||||||
return *this->color_;
|
return *this->color_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -370,21 +370,22 @@ void HighlightingPage::tableCellClicked(const QModelIndex &clicked,
|
|||||||
EditableModelView *view,
|
EditableModelView *view,
|
||||||
HighlightTab tab)
|
HighlightTab tab)
|
||||||
{
|
{
|
||||||
|
if (!clicked.flags().testFlag(Qt::ItemIsEnabled))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (tab)
|
switch (tab)
|
||||||
{
|
{
|
||||||
case HighlightTab::Messages:
|
case HighlightTab::Messages:
|
||||||
case HighlightTab::Users: {
|
case HighlightTab::Users: {
|
||||||
using Column = HighlightModel::Column;
|
using Column = HighlightModel::Column;
|
||||||
bool restrictColorRow =
|
|
||||||
(tab == HighlightTab::Messages &&
|
if (clicked.column() == Column::SoundPath)
|
||||||
clicked.row() ==
|
|
||||||
HighlightModel::HighlightRowIndexes::WhisperRow);
|
|
||||||
if (clicked.column() == Column::SoundPath &&
|
|
||||||
clicked.flags().testFlag(Qt::ItemIsEnabled))
|
|
||||||
{
|
{
|
||||||
this->openSoundDialog(clicked, view, Column::SoundPath);
|
this->openSoundDialog(clicked, view, Column::SoundPath);
|
||||||
}
|
}
|
||||||
else if (clicked.column() == Column::Color && !restrictColorRow)
|
else if (clicked.column() == Column::Color)
|
||||||
{
|
{
|
||||||
this->openColorDialog(clicked, view, tab);
|
this->openColorDialog(clicked, view, tab);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user