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:
@@ -50,6 +50,8 @@ enum class MessageFlag : int64_t {
|
||||
LiveUpdatesAdd = (1LL << 28),
|
||||
LiveUpdatesRemove = (1LL << 29),
|
||||
LiveUpdatesUpdate = (1LL << 30),
|
||||
/// The message caught by AutoMod containing the user who sent the message & its contents
|
||||
AutoModOffendingMessage = (1LL << 31),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
||||
@@ -78,159 +78,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time)
|
||||
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()
|
||||
: message_(std::make_shared<Message>())
|
||||
{
|
||||
|
||||
@@ -53,9 +53,6 @@ const ImageUploaderResultTag imageUploaderResultMessage{};
|
||||
|
||||
MessagePtr makeSystemMessage(const QString &text);
|
||||
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 {
|
||||
bool disablePingSounds = false;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
@@ -170,18 +172,10 @@ void SharedMessageBuilder::parseHighlights()
|
||||
this->highlightAlert_ = highlightResult.alert;
|
||||
|
||||
this->highlightSound_ = highlightResult.playSound;
|
||||
this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl;
|
||||
|
||||
this->message().highlightColor = highlightResult.color;
|
||||
|
||||
if (highlightResult.customSoundUrl)
|
||||
{
|
||||
this->highlightSoundUrl_ = *highlightResult.customSoundUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
|
||||
if (highlightResult.showInMentions)
|
||||
{
|
||||
this->message().flags.set(MessageFlag::ShowInMentions);
|
||||
@@ -199,6 +193,15 @@ void SharedMessageBuilder::appendChannelName()
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -206,21 +209,32 @@ void SharedMessageBuilder::triggerHighlights()
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSettings()->isMutedChannel(this->channel->getName()))
|
||||
if (getSettings()->isMutedChannel(channelName))
|
||||
{
|
||||
// Do nothing. Pings are muted in this channel.
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
const bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QColor>
|
||||
#include <QUrl>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
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
|
||||
virtual void parseHighlights();
|
||||
static void triggerHighlights(const QString &channelName, bool playSound,
|
||||
const std::optional<QUrl> &customSoundUrl,
|
||||
bool windowAlert);
|
||||
|
||||
void appendChannelName();
|
||||
|
||||
@@ -72,8 +77,7 @@ protected:
|
||||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
|
||||
QUrl highlightSoundUrl_;
|
||||
std::optional<QUrl> highlightSoundCustomUrl_{};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -342,9 +342,13 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
||||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
||||
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
||||
{
|
||||
// Blend highlight color with usual background color
|
||||
backgroundColor =
|
||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||
assert(this->message_->highlightColor);
|
||||
if (this->message_->highlightColor)
|
||||
{
|
||||
// Blend highlight color with usual background color
|
||||
backgroundColor =
|
||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||
}
|
||||
}
|
||||
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
||||
ctx.preferences.enableSubHighlight)
|
||||
|
||||
@@ -47,6 +47,12 @@ void MessagePreferences::connectSettings(Settings *settings,
|
||||
},
|
||||
holder);
|
||||
|
||||
settings->enableAutomodHighlight.connect(
|
||||
[this](const auto &newValue) {
|
||||
this->enableAutomodHighlight = newValue;
|
||||
},
|
||||
holder);
|
||||
|
||||
settings->alternateMessages.connect(
|
||||
[this](const auto &newValue) {
|
||||
this->alternateMessages = newValue;
|
||||
|
||||
@@ -39,6 +39,7 @@ struct MessagePreferences {
|
||||
bool enableElevatedMessageHighlight{};
|
||||
bool enableFirstMessageHighlight{};
|
||||
bool enableSubHighlight{};
|
||||
bool enableAutomodHighlight{};
|
||||
|
||||
bool alternateMessages{};
|
||||
bool separateMessages{};
|
||||
|
||||
Reference in New Issue
Block a user