mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
[irc] Partially fix IRC colors (#1594)
Doesn't fix #1379 but it is a big step forward. Needs some "real life" testing, but should be good.
This commit is contained in:
parent
0f9a612c55
commit
e4af009fda
|
@ -156,6 +156,7 @@ SOURCES += \
|
|||
src/messages/MessageColor.cpp \
|
||||
src/messages/MessageContainer.cpp \
|
||||
src/messages/MessageElement.cpp \
|
||||
src/messages/SharedMessageBuilder.cpp \
|
||||
src/messages/search/AuthorPredicate.cpp \
|
||||
src/messages/search/LinkPredicate.cpp \
|
||||
src/messages/search/SubstringPredicate.cpp \
|
||||
|
@ -171,6 +172,7 @@ SOURCES += \
|
|||
src/providers/irc/IrcChannel2.cpp \
|
||||
src/providers/irc/IrcCommands.cpp \
|
||||
src/providers/irc/IrcConnection2.cpp \
|
||||
src/providers/irc/IrcMessageBuilder.cpp \
|
||||
src/providers/irc/IrcServer.cpp \
|
||||
src/providers/LinkResolver.cpp \
|
||||
src/providers/twitch/api/Helix.cpp \
|
||||
|
@ -298,6 +300,7 @@ HEADERS += \
|
|||
src/common/DownloadManager.hpp \
|
||||
src/common/Env.hpp \
|
||||
src/common/FlagsEnum.hpp \
|
||||
src/common/IrcColors.hpp \
|
||||
src/common/LinkParser.hpp \
|
||||
src/common/Modes.hpp \
|
||||
src/common/NetworkCommon.hpp \
|
||||
|
@ -353,6 +356,7 @@ HEADERS += \
|
|||
src/messages/MessageContainer.hpp \
|
||||
src/messages/MessageElement.hpp \
|
||||
src/messages/MessageParseArgs.hpp \
|
||||
src/messages/SharedMessageBuilder.hpp \
|
||||
src/messages/search/AuthorPredicate.hpp \
|
||||
src/messages/search/LinkPredicate.hpp \
|
||||
src/messages/search/MessagePredicate.hpp \
|
||||
|
@ -371,6 +375,7 @@ HEADERS += \
|
|||
src/providers/irc/IrcChannel2.hpp \
|
||||
src/providers/irc/IrcCommands.hpp \
|
||||
src/providers/irc/IrcConnection2.hpp \
|
||||
src/providers/irc/IrcMessageBuilder.hpp \
|
||||
src/providers/irc/IrcServer.hpp \
|
||||
src/providers/LinkResolver.hpp \
|
||||
src/providers/twitch/api/Helix.hpp \
|
||||
|
|
62
src/common/IrcColors.hpp
Normal file
62
src/common/IrcColors.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// Colors taken from https://modern.ircdocs.horse/formatting.html
|
||||
static QMap<int, QColor> IRC_COLORS = {
|
||||
{0, QColor("white")}, {1, QColor("black")},
|
||||
{2, QColor("blue")}, {3, QColor("green")},
|
||||
{4, QColor("red")}, {5, QColor("brown")},
|
||||
{6, QColor("purple")}, {7, QColor("orange")},
|
||||
{8, QColor("yellow")}, {9, QColor("lightgreen")},
|
||||
{10, QColor("cyan")}, {11, QColor("lightcyan")},
|
||||
{12, QColor("lightblue")}, {13, QColor("pink")},
|
||||
{14, QColor("gray")}, {15, QColor("lightgray")},
|
||||
{16, QColor("#470000")}, {17, QColor("#472100")},
|
||||
{18, QColor("#474700")}, {19, QColor("#324700")},
|
||||
{20, QColor("#004700")}, {21, QColor("#00472c")},
|
||||
{22, QColor("#004747")}, {23, QColor("#002747")},
|
||||
{24, QColor("#000047")}, {25, QColor("#2e0047")},
|
||||
{26, QColor("#470047")}, {27, QColor("#47002a")},
|
||||
{28, QColor("#740000")}, {29, QColor("#743a00")},
|
||||
{30, QColor("#747400")}, {31, QColor("#517400")},
|
||||
{32, QColor("#007400")}, {33, QColor("#007449")},
|
||||
{34, QColor("#007474")}, {35, QColor("#004074")},
|
||||
{36, QColor("#000074")}, {37, QColor("#4b0074")},
|
||||
{38, QColor("#740074")}, {39, QColor("#740045")},
|
||||
{40, QColor("#b50000")}, {41, QColor("#b56300")},
|
||||
{42, QColor("#b5b500")}, {43, QColor("#7db500")},
|
||||
{44, QColor("#00b500")}, {45, QColor("#00b571")},
|
||||
{46, QColor("#00b5b5")}, {47, QColor("#0063b5")},
|
||||
{48, QColor("#0000b5")}, {49, QColor("#7500b5")},
|
||||
{50, QColor("#b500b5")}, {51, QColor("#b5006b")},
|
||||
{52, QColor("#ff0000")}, {53, QColor("#ff8c00")},
|
||||
{54, QColor("#ffff00")}, {55, QColor("#b2ff00")},
|
||||
{56, QColor("#00ff00")}, {57, QColor("#00ffa0")},
|
||||
{58, QColor("#00ffff")}, {59, QColor("#008cff")},
|
||||
{60, QColor("#0000ff")}, {61, QColor("#a500ff")},
|
||||
{62, QColor("#ff00ff")}, {63, QColor("#ff0098")},
|
||||
{64, QColor("#ff5959")}, {65, QColor("#ffb459")},
|
||||
{66, QColor("#ffff71")}, {67, QColor("#cfff60")},
|
||||
{68, QColor("#6fff6f")}, {69, QColor("#65ffc9")},
|
||||
{70, QColor("#6dffff")}, {71, QColor("#59b4ff")},
|
||||
{72, QColor("#5959ff")}, {73, QColor("#c459ff")},
|
||||
{74, QColor("#ff66ff")}, {75, QColor("#ff59bc")},
|
||||
{76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")},
|
||||
{78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")},
|
||||
{80, QColor("#9cff9c")}, {81, QColor("#9cffdb")},
|
||||
{82, QColor("#9cffff")}, {83, QColor("#9cd3ff")},
|
||||
{84, QColor("#9c9cff")}, {85, QColor("#dc9cff")},
|
||||
{86, QColor("#ff9cff")}, {87, QColor("#ff94d3")},
|
||||
{88, QColor("#000000")}, {89, QColor("#131313")},
|
||||
{90, QColor("#282828")}, {91, QColor("#363636")},
|
||||
{92, QColor("#4d4d4d")}, {93, QColor("#656565")},
|
||||
{94, QColor("#818181")}, {95, QColor("#9f9f9f")},
|
||||
{96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")},
|
||||
{98, QColor("#ffffff")},
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -34,7 +34,6 @@ struct MessageParseArgs {
|
|||
};
|
||||
|
||||
class MessageBuilder
|
||||
|
||||
{
|
||||
public:
|
||||
MessageBuilder();
|
||||
|
@ -70,4 +69,5 @@ public:
|
|||
private:
|
||||
std::shared_ptr<Message> message_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "messages/MessageElement.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/IrcColors.hpp"
|
||||
#include "debug/Benchmark.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||
|
@ -11,6 +12,14 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QRegularExpression IRC_COLOR_PARSE_REGEX(
|
||||
"\u0003(\\d{1,2})?(,(\\d{1,2}))?",
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
|
||||
} // namespace
|
||||
|
||||
MessageElement::MessageElement(MessageElementFlags flags)
|
||||
: flags_(flags)
|
||||
{
|
||||
|
@ -428,4 +437,200 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
}
|
||||
|
||||
// TEXT
|
||||
// IrcTextElement gets its color from the color code in the message, and can change from character to character.
|
||||
// This differs from the TextElement
|
||||
IrcTextElement::IrcTextElement(const QString &fullText,
|
||||
MessageElementFlags flags, FontStyle style)
|
||||
: MessageElement(flags)
|
||||
, style_(style)
|
||||
{
|
||||
assert(IRC_COLOR_PARSE_REGEX.isValid());
|
||||
|
||||
// Default pen colors. -1 = default theme colors
|
||||
int fg = -1, bg = -1;
|
||||
|
||||
// Split up the message in words (space separated)
|
||||
// Each word contains one or more colored segments.
|
||||
// The color of that segment is "global", as in it can be decided by the word before it.
|
||||
for (const auto &text : fullText.split(' '))
|
||||
{
|
||||
std::vector<Segment> segments;
|
||||
|
||||
int pos = 0;
|
||||
int lastPos = 0;
|
||||
|
||||
auto i = IRC_COLOR_PARSE_REGEX.globalMatch(text);
|
||||
|
||||
while (i.hasNext())
|
||||
{
|
||||
auto match = i.next();
|
||||
|
||||
if (lastPos != match.capturedStart() && match.capturedStart() != 0)
|
||||
{
|
||||
auto seg = Segment{};
|
||||
seg.text = text.mid(lastPos, match.capturedStart() - lastPos);
|
||||
seg.fg = fg;
|
||||
seg.bg = bg;
|
||||
segments.emplace_back(seg);
|
||||
lastPos = match.capturedStart() + match.capturedLength();
|
||||
}
|
||||
|
||||
if (!match.captured(1).isEmpty())
|
||||
{
|
||||
fg = match.captured(1).toInt(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
fg = -1;
|
||||
}
|
||||
if (!match.captured(3).isEmpty())
|
||||
{
|
||||
bg = match.captured(3).toInt(nullptr);
|
||||
}
|
||||
else if (fg == -1)
|
||||
{
|
||||
bg = -1;
|
||||
}
|
||||
|
||||
lastPos = match.capturedStart() + match.capturedLength();
|
||||
}
|
||||
|
||||
auto seg = Segment{};
|
||||
seg.text = text.mid(lastPos);
|
||||
seg.fg = fg;
|
||||
seg.bg = bg;
|
||||
segments.emplace_back(seg);
|
||||
|
||||
QString n(text);
|
||||
|
||||
n.replace(IRC_COLOR_PARSE_REGEX, "");
|
||||
|
||||
Word w{
|
||||
n,
|
||||
-1,
|
||||
segments,
|
||||
};
|
||||
this->words_.emplace_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
void IrcTextElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
MessageColor defaultColorType = MessageColor::Text;
|
||||
auto defaultColor = defaultColorType.getColor(*app->themes);
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
QFontMetrics metrics =
|
||||
app->fonts->getFontMetrics(this->style_, container.getScale());
|
||||
|
||||
for (auto &word : this->words_)
|
||||
{
|
||||
auto getTextLayoutElement = [&](QString text,
|
||||
std::vector<Segment> segments,
|
||||
int width, bool hasTrailingSpace) {
|
||||
std::vector<PajSegment> xd{};
|
||||
|
||||
for (const auto &segment : segments)
|
||||
{
|
||||
QColor color = defaultColor;
|
||||
if (segment.fg >= 0 && segment.fg <= 98)
|
||||
{
|
||||
color = IRC_COLORS[segment.fg];
|
||||
}
|
||||
app->themes->normalizeColor(color);
|
||||
xd.emplace_back(PajSegment{segment.text, color});
|
||||
}
|
||||
|
||||
auto e = (new MultiColorTextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), xd,
|
||||
this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
e->setTrailingSpace(true);
|
||||
e->setText(text);
|
||||
|
||||
// If URL link was changed,
|
||||
// Should update it in MessageLayoutElement too!
|
||||
if (this->getLink().type == Link::Url)
|
||||
{
|
||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
// fourtf: add again
|
||||
// if (word.width == -1) {
|
||||
word.width = metrics.width(word.text);
|
||||
// }
|
||||
|
||||
// see if the text fits in the current line
|
||||
if (container.fitsInLine(word.width))
|
||||
{
|
||||
container.addElementNoLineBreak(
|
||||
getTextLayoutElement(word.text, word.segments, word.width,
|
||||
this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
|
||||
// see if the text fits in the next line
|
||||
if (!container.atStartOfLine())
|
||||
{
|
||||
container.breakLine();
|
||||
|
||||
if (container.fitsInLine(word.width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.segments, word.width,
|
||||
this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// we done goofed, we need to wrap the text
|
||||
QString text = word.text;
|
||||
int textLength = text.length();
|
||||
int wordStart = 0;
|
||||
int width = 0;
|
||||
|
||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
||||
|
||||
// XXX(pajlada): NOT TESTED
|
||||
for (int i = 0; i < textLength; i++) //
|
||||
{
|
||||
auto isSurrogate = text.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(text[i].unicode());
|
||||
|
||||
auto charWidth = isSurrogate ? metrics.width(text.mid(i, 2))
|
||||
: metrics.width(text[i]);
|
||||
|
||||
if (!container.fitsInLine(width + charWidth)) //
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
text.mid(wordStart, i - wordStart), {}, width, false));
|
||||
container.breakLine();
|
||||
|
||||
wordStart = i;
|
||||
width = charWidth;
|
||||
|
||||
if (isSurrogate)
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
width += charWidth;
|
||||
|
||||
if (isSurrogate)
|
||||
i++;
|
||||
}
|
||||
|
||||
container.addElement(getTextLayoutElement(
|
||||
text.mid(wordStart), {}, width, this->hasTrailingSpace()));
|
||||
container.breakLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -291,4 +291,34 @@ public:
|
|||
MessageElementFlags flags) override;
|
||||
};
|
||||
|
||||
// contains a full message string that's split into words on space and parses irc colors that are then put into segments
|
||||
// these segments are later passed to "MultiColorTextLayoutElement" elements to be rendered :)
|
||||
class IrcTextElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
IrcTextElement(const QString &text, MessageElementFlags flags,
|
||||
FontStyle style = FontStyle::ChatMedium);
|
||||
~IrcTextElement() override = default;
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
private:
|
||||
FontStyle style_;
|
||||
|
||||
struct Segment {
|
||||
QString text;
|
||||
int fg = -1;
|
||||
int bg = -1;
|
||||
};
|
||||
|
||||
struct Word {
|
||||
QString text;
|
||||
int width = -1;
|
||||
std::vector<Segment> segments;
|
||||
};
|
||||
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
399
src/messages/SharedMessageBuilder.cpp
Normal file
399
src/messages/SharedMessageBuilder.cpp
Normal file
|
@ -0,0 +1,399 @@
|
|||
#include "messages/SharedMessageBuilder.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QUrl getFallbackHighlightSound()
|
||||
{
|
||||
QString path = getSettings()->pathHighlightSound;
|
||||
bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
|
||||
|
||||
// Use fallback sound when checkbox is not checked
|
||||
// or custom file doesn't exist
|
||||
if (getSettings()->customHighlightSound && fileExists)
|
||||
{
|
||||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return QUrl("qrc:/sounds/ping2.wav");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SharedMessageBuilder::SharedMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
: channel(_channel)
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(_ircMessage->content())
|
||||
, action_(_ircMessage->isAction())
|
||||
{
|
||||
}
|
||||
|
||||
SharedMessageBuilder::SharedMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args, QString content, bool isAction)
|
||||
: channel(_channel)
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(content)
|
||||
, action_(isAction)
|
||||
{
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
QColor getRandomColor(const QString &v)
|
||||
{
|
||||
int colorSeed = 0;
|
||||
for (const auto &c : v)
|
||||
{
|
||||
colorSeed += c.digitValue();
|
||||
}
|
||||
const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size();
|
||||
return TWITCH_USERNAME_COLORS[colorIndex];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SharedMessageBuilder::parse()
|
||||
{
|
||||
this->parseUsernameColor();
|
||||
|
||||
this->parseUsername();
|
||||
|
||||
this->message().flags.set(MessageFlag::Collapsed);
|
||||
}
|
||||
|
||||
bool SharedMessageBuilder::isIgnored() const
|
||||
{
|
||||
// TODO(pajlada): Do we need to check if the phrase is valid first?
|
||||
auto phrases = getCSettings().ignoredMessages.readOnly();
|
||||
for (const auto &phrase : *phrases)
|
||||
{
|
||||
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
|
||||
{
|
||||
qDebug() << "Blocking message because it contains ignored phrase"
|
||||
<< phrase.getPattern();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseUsernameColor()
|
||||
{
|
||||
if (getSettings()->colorizeNicknames)
|
||||
{
|
||||
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseUsername()
|
||||
{
|
||||
// username
|
||||
this->userName = this->ircMessage->nick();
|
||||
|
||||
this->message().loginName = this->userName;
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseHighlights()
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
if (this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight)
|
||||
{
|
||||
if (getSettings()->enableSubHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableSubHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->subHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Subscription);
|
||||
|
||||
// This message was a subscription.
|
||||
// Don't check for any other highlight phrases.
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: Non-common term in SharedMessageBuilder
|
||||
auto currentUser = app->accounts->twitch.getCurrent();
|
||||
|
||||
QString currentUsername = currentUser->getUserName();
|
||||
|
||||
if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
|
||||
{
|
||||
// Do nothing. We ignore highlights from this user.
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight because it's a whisper
|
||||
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
|
||||
{
|
||||
if (getSettings()->enableWhisperHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableWhisperHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Whisper);
|
||||
|
||||
/*
|
||||
* Do _NOT_ return yet, we might want to apply phrase/user name
|
||||
* highlights (which override whisper color/sound).
|
||||
*/
|
||||
}
|
||||
|
||||
// Highlight because of sender
|
||||
auto userHighlights = getCSettings().highlightedUsers.readOnly();
|
||||
for (const HighlightPhrase &userHighlight : *userHighlights)
|
||||
{
|
||||
if (!userHighlight.isMatch(this->ircMessage->nick()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
qDebug() << "Highlight because user" << this->ircMessage->nick()
|
||||
<< "sent a message";
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = userHighlight.getColor();
|
||||
|
||||
if (userHighlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (userHighlight.hasSound())
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
// Use custom sound if set, otherwise use the fallback sound
|
||||
if (userHighlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = userHighlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* User name highlights "beat" highlight phrases: If a message has
|
||||
* all attributes (color, taskbar flashing, sound) set, highlight
|
||||
* phrases will not be checked.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->ircMessage->nick() == currentUsername)
|
||||
{
|
||||
// Do nothing. Highlights cannot be triggered by yourself
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This vector should only be rebuilt upon highlights being changed
|
||||
// fourtf: should be implemented in the HighlightsController
|
||||
std::vector<HighlightPhrase> activeHighlights =
|
||||
getSettings()->highlightedMessages.cloneVector();
|
||||
|
||||
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
|
||||
{
|
||||
HighlightPhrase selfHighlight(
|
||||
currentUsername, getSettings()->enableSelfHighlightTaskbar,
|
||||
getSettings()->enableSelfHighlightSound, false, false,
|
||||
getSettings()->selfHighlightSoundUrl.getValue(),
|
||||
ColorProvider::instance().color(ColorType::SelfHighlight));
|
||||
activeHighlights.emplace_back(std::move(selfHighlight));
|
||||
}
|
||||
|
||||
// Highlight because of message
|
||||
for (const HighlightPhrase &highlight : activeHighlights)
|
||||
{
|
||||
if (!highlight.isMatch(this->originalMessage_))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
|
||||
if (highlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
// Only set highlightSound_ if it hasn't been set by username
|
||||
// highlights already.
|
||||
if (highlight.hasSound() && !this->highlightSound_)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback sound
|
||||
if (highlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = highlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* Break once no further attributes (taskbar, sound) can be
|
||||
* applied.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||
{
|
||||
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::addTextOrEmoji(const QString &string_)
|
||||
{
|
||||
auto string = QString(string_);
|
||||
|
||||
// Actually just text
|
||||
auto linkString = this->matchLink(string);
|
||||
auto link = Link();
|
||||
auto textColor = this->action_ ? MessageColor(this->usernameColor_)
|
||||
: MessageColor(MessageColor::Text);
|
||||
|
||||
if (linkString.isEmpty())
|
||||
{
|
||||
if (string.startsWith('@'))
|
||||
{
|
||||
this->emplace<TextElement>(string, MessageElementFlag::BoldUsername,
|
||||
textColor, FontStyle::ChatMediumBold);
|
||||
this->emplace<TextElement>(
|
||||
string, MessageElementFlag::NonBoldUsername, textColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->emplace<TextElement>(string, MessageElementFlag::Text,
|
||||
textColor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this->addLink(string, linkString);
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::appendChannelName()
|
||||
{
|
||||
QString channelName("#" + this->channel->getName());
|
||||
Link link(Link::Url, this->channel->getName() + "\n" + this->message().id);
|
||||
|
||||
this->emplace<TextElement>(channelName, MessageElementFlag::ChannelName,
|
||||
MessageColor::System) //
|
||||
->setLink(link);
|
||||
}
|
||||
|
||||
inline QMediaPlayer *getPlayer()
|
||||
{
|
||||
if (isGuiThread())
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
return player;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::triggerHighlights()
|
||||
{
|
||||
static QUrl currentPlayerUrl;
|
||||
|
||||
if (getCSettings().isMutedChannel(this->channel->getName()))
|
||||
{
|
||||
// Do nothing. Pings are muted in this channel.
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
{
|
||||
if (auto player = getPlayer())
|
||||
{
|
||||
// update the media player url if necessary
|
||||
if (currentPlayerUrl != this->highlightSoundUrl_)
|
||||
{
|
||||
player->setMedia(this->highlightSoundUrl_);
|
||||
|
||||
currentPlayerUrl = this->highlightSoundUrl_;
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
69
src/messages/SharedMessageBuilder.hpp
Normal file
69
src/messages/SharedMessageBuilder.hpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "messages/MessageBuilder.hpp"
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <QColor>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class SharedMessageBuilder : public MessageBuilder
|
||||
{
|
||||
public:
|
||||
SharedMessageBuilder() = delete;
|
||||
|
||||
explicit SharedMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args);
|
||||
|
||||
explicit SharedMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args,
|
||||
QString content, bool isAction);
|
||||
|
||||
QString userName;
|
||||
|
||||
[[nodiscard]] virtual bool isIgnored() const;
|
||||
|
||||
// triggerHighlights triggers any alerts or sounds parsed by parseHighlights
|
||||
virtual void triggerHighlights();
|
||||
virtual MessagePtr build() = 0;
|
||||
|
||||
protected:
|
||||
virtual void parse();
|
||||
|
||||
virtual void parseUsernameColor();
|
||||
|
||||
virtual void parseUsername();
|
||||
|
||||
virtual Outcome tryAppendEmote(const EmoteName &name)
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
|
||||
// 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 addTextOrEmoji(EmotePtr emote);
|
||||
virtual void addTextOrEmoji(const QString &value);
|
||||
|
||||
void appendChannelName();
|
||||
|
||||
Channel *channel;
|
||||
const Communi::IrcMessage *ircMessage;
|
||||
MessageParseArgs args;
|
||||
const QVariantMap tags;
|
||||
QString originalMessage_;
|
||||
|
||||
const bool action_{};
|
||||
|
||||
QColor usernameColor_;
|
||||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
|
||||
QUrl highlightSoundUrl_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index)
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TEXT
|
||||
//
|
||||
|
||||
MultiColorTextLayoutElement::MultiColorTextLayoutElement(
|
||||
MessageElement &_creator, QString &_text, const QSize &_size,
|
||||
std::vector<PajSegment> segments, FontStyle _style, float _scale)
|
||||
: TextLayoutElement(_creator, _text, _size, QColor{}, _style, _scale)
|
||||
, segments_(segments)
|
||||
{
|
||||
this->setText(_text);
|
||||
}
|
||||
|
||||
void MultiColorTextLayoutElement::paint(QPainter &painter)
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
painter.setPen(this->color_);
|
||||
|
||||
painter.setFont(app->fonts->getFont(this->style_, this->scale_));
|
||||
|
||||
int xOffset = 0;
|
||||
|
||||
auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_);
|
||||
|
||||
for (const auto &segment : this->segments_)
|
||||
{
|
||||
// qDebug() << "Draw segment:" << segment.text;
|
||||
painter.setPen(segment.color);
|
||||
painter.drawText(QRectF(this->getRect().x() + xOffset,
|
||||
this->getRect().y(), 10000, 10000),
|
||||
segment.text,
|
||||
QTextOption(Qt::AlignLeft | Qt::AlignTop));
|
||||
xOffset += metrics.width(segment.text);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -107,7 +107,6 @@ protected:
|
|||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(int index) override;
|
||||
|
||||
private:
|
||||
QColor color_;
|
||||
FontStyle style_;
|
||||
float scale_;
|
||||
|
@ -138,4 +137,25 @@ private:
|
|||
QString line2;
|
||||
};
|
||||
|
||||
struct PajSegment {
|
||||
QString text;
|
||||
QColor color;
|
||||
};
|
||||
|
||||
// TEXT
|
||||
class MultiColorTextLayoutElement : public TextLayoutElement
|
||||
{
|
||||
public:
|
||||
MultiColorTextLayoutElement(MessageElement &creator_, QString &text,
|
||||
const QSize &size,
|
||||
std::vector<PajSegment> segments,
|
||||
FontStyle style_, float scale_);
|
||||
|
||||
protected:
|
||||
void paint(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
std::vector<PajSegment> segments_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
93
src/providers/irc/IrcMessageBuilder.cpp
Normal file
93
src/providers/irc/IrcMessageBuilder.cpp
Normal file
|
@ -0,0 +1,93 @@
|
|||
#include "providers/irc/IrcMessageBuilder.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/IrcHelpers.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
IrcMessageBuilder::IrcMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
: SharedMessageBuilder(_channel, _ircMessage, _args)
|
||||
{
|
||||
this->usernameColor_ = getApp()->themes->messages.textColors.system;
|
||||
}
|
||||
|
||||
IrcMessageBuilder::IrcMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args,
|
||||
QString content, bool isAction)
|
||||
: SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction)
|
||||
{
|
||||
assert(false);
|
||||
this->usernameColor_ = getApp()->themes->messages.textColors.system;
|
||||
}
|
||||
|
||||
MessagePtr IrcMessageBuilder::build()
|
||||
{
|
||||
// PARSE
|
||||
this->parse();
|
||||
|
||||
// PUSH ELEMENTS
|
||||
this->appendChannelName();
|
||||
|
||||
this->emplace<TimestampElement>();
|
||||
|
||||
this->appendUsername();
|
||||
|
||||
// words
|
||||
this->addWords(this->originalMessage_.split(' '));
|
||||
|
||||
this->message().messageText = this->originalMessage_;
|
||||
this->message().searchText = this->message().localizedName + " " +
|
||||
this->userName + ": " + this->originalMessage_;
|
||||
|
||||
// highlights
|
||||
this->parseHighlights();
|
||||
|
||||
// highlighting incoming whispers if requested per setting
|
||||
if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers)
|
||||
{
|
||||
this->message().flags.set(MessageFlag::HighlightedWhisper, true);
|
||||
}
|
||||
|
||||
return this->release();
|
||||
}
|
||||
|
||||
void IrcMessageBuilder::addWords(const QStringList &words)
|
||||
{
|
||||
this->emplace<IrcTextElement>(words.join(' '), MessageElementFlag::Text);
|
||||
}
|
||||
|
||||
void IrcMessageBuilder::appendUsername()
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
QString username = this->userName;
|
||||
this->message().loginName = username;
|
||||
|
||||
// The full string that will be rendered in the chat widget
|
||||
QString usernameText = username;
|
||||
|
||||
if (!this->action_)
|
||||
{
|
||||
usernameText += ":";
|
||||
}
|
||||
|
||||
this->emplace<TextElement>(usernameText, MessageElementFlag::Username,
|
||||
this->usernameColor_, FontStyle::ChatMediumBold)
|
||||
->setLink({Link::UserInfo, this->message().displayName});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
41
src/providers/irc/IrcMessageBuilder.hpp
Normal file
41
src/providers/irc/IrcMessageBuilder.hpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "messages/SharedMessageBuilder.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Emote;
|
||||
using EmotePtr = std::shared_ptr<const Emote>;
|
||||
|
||||
class Channel;
|
||||
class TwitchChannel;
|
||||
|
||||
class IrcMessageBuilder : public SharedMessageBuilder
|
||||
{
|
||||
public:
|
||||
IrcMessageBuilder() = delete;
|
||||
|
||||
explicit IrcMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args);
|
||||
explicit IrcMessageBuilder(Channel *_channel,
|
||||
const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args, QString content,
|
||||
bool isAction);
|
||||
|
||||
MessagePtr build() override;
|
||||
|
||||
private:
|
||||
void appendUsername();
|
||||
|
||||
void addWords(const QStringList &words);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -4,9 +4,9 @@
|
|||
#include <cstdlib>
|
||||
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/irc/Irc2.hpp"
|
||||
#include "providers/irc/IrcChannel2.hpp"
|
||||
#include "providers/irc/IrcMessageBuilder.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/QObjectRef.hpp"
|
||||
|
||||
|
@ -86,6 +86,7 @@ void IrcServer::initializeConnectionSignals(IrcConnection *connection,
|
|||
|
||||
QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
|
||||
this, [this](Communi::IrcNoticeMessage *message) {
|
||||
// XD PAJLADA
|
||||
MessageBuilder builder;
|
||||
|
||||
builder.emplace<TimestampElement>();
|
||||
|
@ -176,15 +177,18 @@ void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
|
|||
|
||||
if (auto channel = this->getChannelOrEmpty(target); !channel->isEmpty())
|
||||
{
|
||||
MessageBuilder builder;
|
||||
MessageParseArgs args;
|
||||
IrcMessageBuilder builder(channel.get(), message, args);
|
||||
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.emplace<TextElement>(message->nick() + ":",
|
||||
MessageElementFlag::Username);
|
||||
builder.emplace<TextElement>(message->content(),
|
||||
MessageElementFlag::Text);
|
||||
|
||||
channel->addMessage(builder.release());
|
||||
if (!builder.isIgnored())
|
||||
{
|
||||
builder.triggerHighlights();
|
||||
channel->addMessage(builder.build());
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "message ignored :rage:";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,8 +209,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message)
|
|||
{
|
||||
if (message->nick() == this->data_->nick)
|
||||
{
|
||||
shared->addMessage(
|
||||
MessageBuilder(systemMessage, "joined").release());
|
||||
shared->addMessage(makeSystemMessage("joined"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -230,8 +233,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message)
|
|||
{
|
||||
if (message->nick() == this->data_->nick)
|
||||
{
|
||||
shared->addMessage(
|
||||
MessageBuilder(systemMessage, "parted").release());
|
||||
shared->addMessage(makeSystemMessage("parted"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -11,4 +12,22 @@ inline QByteArray getDefaultClientID()
|
|||
return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal");
|
||||
}
|
||||
|
||||
static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
|
||||
{255, 0, 0}, // Red
|
||||
{0, 0, 255}, // Blue
|
||||
{0, 255, 0}, // Green
|
||||
{178, 34, 34}, // FireBrick
|
||||
{255, 127, 80}, // Coral
|
||||
{154, 205, 50}, // YellowGreen
|
||||
{255, 69, 0}, // OrangeRed
|
||||
{46, 139, 87}, // SeaGreen
|
||||
{218, 165, 32}, // GoldenRod
|
||||
{210, 105, 30}, // Chocolate
|
||||
{95, 158, 160}, // CadetBlue
|
||||
{30, 144, 255}, // DodgerBlue
|
||||
{255, 105, 180}, // HotPink
|
||||
{138, 43, 226}, // BlueViolet
|
||||
{0, 255, 127}, // SpringGreen
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
// using namespace Communi;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
|
@ -31,65 +32,27 @@ const QSet<QString> zeroWidthEmotes{
|
|||
"ReinDeer", "CandyCane", "cvMask", "cvHazmat",
|
||||
};
|
||||
|
||||
QColor getRandomColor(const QVariant &userId)
|
||||
{
|
||||
static const std::vector<QColor> twitchUsernameColors = {
|
||||
{255, 0, 0}, // Red
|
||||
{0, 0, 255}, // Blue
|
||||
{0, 255, 0}, // Green
|
||||
{178, 34, 34}, // FireBrick
|
||||
{255, 127, 80}, // Coral
|
||||
{154, 205, 50}, // YellowGreen
|
||||
{255, 69, 0}, // OrangeRed
|
||||
{46, 139, 87}, // SeaGreen
|
||||
{218, 165, 32}, // GoldenRod
|
||||
{210, 105, 30}, // Chocolate
|
||||
{95, 158, 160}, // CadetBlue
|
||||
{30, 144, 255}, // DodgerBlue
|
||||
{255, 105, 180}, // HotPink
|
||||
{138, 43, 226}, // BlueViolet
|
||||
{0, 255, 127}, // SpringGreen
|
||||
};
|
||||
|
||||
bool ok = true;
|
||||
int colorSeed = userId.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
// We were unable to convert the user ID to an integer, this means Twitch has decided to start using non-integer user IDs
|
||||
// Just randomize the users color
|
||||
colorSeed = std::rand();
|
||||
}
|
||||
|
||||
assert(twitchUsernameColors.size() != 0);
|
||||
const auto colorIndex = colorSeed % twitchUsernameColors.size();
|
||||
return twitchUsernameColors[colorIndex];
|
||||
}
|
||||
|
||||
QUrl getFallbackHighlightSound()
|
||||
{
|
||||
using namespace chatterino;
|
||||
|
||||
QString path = getSettings()->pathHighlightSound;
|
||||
bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
|
||||
|
||||
// Use fallback sound when checkbox is not checked
|
||||
// or custom file doesn't exist
|
||||
if (getSettings()->customHighlightSound && fileExists)
|
||||
{
|
||||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return QUrl("qrc:/sounds/ping2.wav");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QColor getRandomColor(const QVariant &userId)
|
||||
{
|
||||
bool ok = true;
|
||||
int colorSeed = userId.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
// We were unable to convert the user ID to an integer, this means Twitch has decided to start using non-integer user IDs
|
||||
// Just randomize the users color
|
||||
colorSeed = std::rand();
|
||||
}
|
||||
|
||||
const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size();
|
||||
return TWITCH_USERNAME_COLORS[colorIndex];
|
||||
}
|
||||
|
||||
QStringList parseTagList(const QVariantMap &tags, const QString &key)
|
||||
{
|
||||
auto iterator = tags.find(key);
|
||||
|
@ -141,13 +104,8 @@ namespace {
|
|||
TwitchMessageBuilder::TwitchMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
: channel(_channel)
|
||||
: SharedMessageBuilder(_channel, _ircMessage, _args)
|
||||
, twitchChannel(dynamic_cast<TwitchChannel *>(_channel))
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(_ircMessage->content())
|
||||
, action_(_ircMessage->isAction())
|
||||
{
|
||||
this->usernameColor_ = getApp()->themes->messages.textColors.system;
|
||||
}
|
||||
|
@ -155,31 +113,21 @@ TwitchMessageBuilder::TwitchMessageBuilder(
|
|||
TwitchMessageBuilder::TwitchMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args, QString content, bool isAction)
|
||||
: channel(_channel)
|
||||
: SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction)
|
||||
, twitchChannel(dynamic_cast<TwitchChannel *>(_channel))
|
||||
, ircMessage(_ircMessage)
|
||||
, args(_args)
|
||||
, tags(this->ircMessage->tags())
|
||||
, originalMessage_(content)
|
||||
, action_(isAction)
|
||||
{
|
||||
this->usernameColor_ = getApp()->themes->messages.textColors.system;
|
||||
}
|
||||
|
||||
bool TwitchMessageBuilder::isIgnored() const
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
// TODO(pajlada): Do we need to check if the phrase is valid first?
|
||||
auto phrases = getCSettings().ignoredMessages.readOnly();
|
||||
for (const auto &phrase : *phrases)
|
||||
if (SharedMessageBuilder::isIgnored())
|
||||
{
|
||||
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto app = getApp();
|
||||
|
||||
if (getSettings()->enableTwitchIgnoredUsers &&
|
||||
this->tags.contains("user-id"))
|
||||
{
|
||||
|
@ -214,75 +162,29 @@ bool TwitchMessageBuilder::isIgnored() const
|
|||
return false;
|
||||
}
|
||||
|
||||
inline QMediaPlayer *getPlayer()
|
||||
{
|
||||
if (isGuiThread())
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
return player;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::triggerHighlights()
|
||||
{
|
||||
static QUrl currentPlayerUrl;
|
||||
|
||||
if (this->historicalMessage_)
|
||||
{
|
||||
// Do nothing. Highlights should not be triggered on historical messages.
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCSettings().isMutedChannel(this->channel->getName()))
|
||||
{
|
||||
// Do nothing. Pings are muted in this channel.
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
{
|
||||
if (auto player = getPlayer())
|
||||
{
|
||||
// update the media player url if necessary
|
||||
if (currentPlayerUrl != this->highlightSoundUrl_)
|
||||
{
|
||||
player->setMedia(this->highlightSoundUrl_);
|
||||
|
||||
currentPlayerUrl = this->highlightSoundUrl_;
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
SharedMessageBuilder::triggerHighlights();
|
||||
}
|
||||
|
||||
MessagePtr TwitchMessageBuilder::build()
|
||||
{
|
||||
// PARSING
|
||||
// PARSE
|
||||
this->userId_ = this->ircMessage->tag("user-id").toString();
|
||||
|
||||
this->parseUsername();
|
||||
this->parse();
|
||||
|
||||
if (this->userName == this->channel->getName())
|
||||
{
|
||||
this->senderIsBroadcaster = true;
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Collapsed);
|
||||
|
||||
// PARSING
|
||||
this->parseMessageID();
|
||||
|
||||
this->parseRoomID();
|
||||
|
@ -479,7 +381,7 @@ void TwitchMessageBuilder::addWords(
|
|||
|
||||
void TwitchMessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||
{
|
||||
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
||||
return SharedMessageBuilder::addTextOrEmoji(emote);
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
|
||||
|
@ -528,40 +430,6 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
|
|||
{
|
||||
this->addLink(string, linkString);
|
||||
}
|
||||
|
||||
// if (!linkString.isEmpty()) {
|
||||
// if (getSettings()->lowercaseLink) {
|
||||
// QRegularExpression httpRegex("\\bhttps?://",
|
||||
// QRegularExpression::CaseInsensitiveOption); QRegularExpression
|
||||
// ftpRegex("\\bftps?://",
|
||||
// QRegularExpression::CaseInsensitiveOption); QRegularExpression
|
||||
// getDomain("\\/\\/([^\\/]*)"); QString tempString = string;
|
||||
|
||||
// if (!string.contains(httpRegex)) {
|
||||
// if (!string.contains(ftpRegex)) {
|
||||
// tempString.insert(0, "http://");
|
||||
// }
|
||||
// }
|
||||
// QString domain = getDomain.match(tempString).captured(1);
|
||||
// string.replace(domain, domain.toLower());
|
||||
// }
|
||||
// link = Link(Link::Url, linkString);
|
||||
// textColor = MessageColor(MessageColor::Link);
|
||||
//}
|
||||
// if (string.startsWith('@')) {
|
||||
// this->emplace<TextElement>(string, MessageElementFlag::BoldUsername,
|
||||
// textColor,
|
||||
// FontStyle::ChatMediumBold) //
|
||||
// ->setLink(link);
|
||||
// this->emplace<TextElement>(string,
|
||||
// MessageElementFlag::NonBoldUsername,
|
||||
// textColor) //
|
||||
// ->setLink(link);
|
||||
//} else {
|
||||
// this->emplace<TextElement>(string, MessageElementFlag::Text,
|
||||
// textColor) //
|
||||
// ->setLink(link);
|
||||
//}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::parseMessageID()
|
||||
|
@ -594,16 +462,6 @@ void TwitchMessageBuilder::parseRoomID()
|
|||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::appendChannelName()
|
||||
{
|
||||
QString channelName("#" + this->channel->getName());
|
||||
Link link(Link::Url, this->channel->getName() + "\n" + this->message().id);
|
||||
|
||||
this->emplace<TextElement>(channelName, MessageElementFlag::ChannelName,
|
||||
MessageColor::System) //
|
||||
->setLink(link);
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::parseUsernameColor()
|
||||
{
|
||||
const auto iterator = this->tags.find("color");
|
||||
|
@ -624,10 +482,7 @@ void TwitchMessageBuilder::parseUsernameColor()
|
|||
|
||||
void TwitchMessageBuilder::parseUsername()
|
||||
{
|
||||
this->parseUsernameColor();
|
||||
|
||||
// username
|
||||
this->userName = this->ircMessage->nick();
|
||||
SharedMessageBuilder::parseUsername();
|
||||
|
||||
if (this->userName.isEmpty() || this->args.trimSubscriberUsername)
|
||||
{
|
||||
|
@ -983,193 +838,6 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::parseHighlights()
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
if (this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight)
|
||||
{
|
||||
if (getSettings()->enableSubHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableSubHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->subHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Subscription);
|
||||
|
||||
// This message was a subscription.
|
||||
// Don't check for any other highlight phrases.
|
||||
return;
|
||||
}
|
||||
|
||||
auto currentUser = app->accounts->twitch.getCurrent();
|
||||
|
||||
QString currentUsername = currentUser->getUserName();
|
||||
|
||||
if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
|
||||
{
|
||||
// Do nothing. We ignore highlights from this user.
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight because it's a whisper
|
||||
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
|
||||
{
|
||||
if (getSettings()->enableWhisperHighlightTaskbar)
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (getSettings()->enableWhisperHighlightSound)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback
|
||||
if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
|
||||
{
|
||||
this->highlightSoundUrl_ =
|
||||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
this->message().highlightColor =
|
||||
ColorProvider::instance().color(ColorType::Whisper);
|
||||
|
||||
/*
|
||||
* Do _NOT_ return yet, we might want to apply phrase/user name
|
||||
* highlights (which override whisper color/sound).
|
||||
*/
|
||||
}
|
||||
|
||||
// Highlight because of sender
|
||||
auto userHighlights = getCSettings().highlightedUsers.readOnly();
|
||||
for (const HighlightPhrase &userHighlight : *userHighlights)
|
||||
{
|
||||
if (!userHighlight.isMatch(this->ircMessage->nick()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = userHighlight.getColor();
|
||||
|
||||
if (userHighlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
if (userHighlight.hasSound())
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
// Use custom sound if set, otherwise use the fallback sound
|
||||
if (userHighlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = userHighlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* User name highlights "beat" highlight phrases: If a message has
|
||||
* all attributes (color, taskbar flashing, sound) set, highlight
|
||||
* phrases will not be checked.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->ircMessage->nick() == currentUsername)
|
||||
{
|
||||
// Do nothing. Highlights cannot be triggered by yourself
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This vector should only be rebuilt upon highlights being changed
|
||||
// fourtf: should be implemented in the HighlightsController
|
||||
std::vector<HighlightPhrase> activeHighlights =
|
||||
getSettings()->highlightedMessages.cloneVector();
|
||||
|
||||
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
|
||||
{
|
||||
HighlightPhrase selfHighlight(
|
||||
currentUsername, getSettings()->enableSelfHighlightTaskbar,
|
||||
getSettings()->enableSelfHighlightSound, false, false,
|
||||
getSettings()->selfHighlightSoundUrl.getValue(),
|
||||
ColorProvider::instance().color(ColorType::SelfHighlight));
|
||||
activeHighlights.emplace_back(std::move(selfHighlight));
|
||||
}
|
||||
|
||||
// Highlight because of message
|
||||
for (const HighlightPhrase &highlight : activeHighlights)
|
||||
{
|
||||
if (!highlight.isMatch(this->originalMessage_))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
|
||||
if (highlight.hasAlert())
|
||||
{
|
||||
this->highlightAlert_ = true;
|
||||
}
|
||||
|
||||
// Only set highlightSound_ if it hasn't been set by username
|
||||
// highlights already.
|
||||
if (highlight.hasSound() && !this->highlightSound_)
|
||||
{
|
||||
this->highlightSound_ = true;
|
||||
|
||||
// Use custom sound if set, otherwise use fallback sound
|
||||
if (highlight.hasCustomSound())
|
||||
{
|
||||
this->highlightSoundUrl_ = highlight.getSoundUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->highlightAlert_ && this->highlightSound_)
|
||||
{
|
||||
/*
|
||||
* Break once no further attributes (taskbar, sound) can be
|
||||
* applied.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::appendTwitchEmote(
|
||||
const QString &emote,
|
||||
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "messages/SharedMessageBuilder.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
|
@ -17,7 +17,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
|||
class Channel;
|
||||
class TwitchChannel;
|
||||
|
||||
class TwitchMessageBuilder : public MessageBuilder
|
||||
class TwitchMessageBuilder : public SharedMessageBuilder
|
||||
{
|
||||
public:
|
||||
enum UsernameDisplayMode : int {
|
||||
|
@ -36,30 +36,20 @@ public:
|
|||
const MessageParseArgs &_args,
|
||||
QString content, bool isAction);
|
||||
|
||||
Channel *channel;
|
||||
TwitchChannel *twitchChannel;
|
||||
const Communi::IrcMessage *ircMessage;
|
||||
MessageParseArgs args;
|
||||
const QVariantMap tags;
|
||||
|
||||
QString userName;
|
||||
|
||||
[[nodiscard]] bool isIgnored() const;
|
||||
// triggerHighlights triggers any alerts or sounds parsed by parseHighlights
|
||||
void triggerHighlights();
|
||||
MessagePtr build();
|
||||
[[nodiscard]] bool isIgnored() const override;
|
||||
void triggerHighlights() override;
|
||||
MessagePtr build() override;
|
||||
|
||||
private:
|
||||
void parseUsernameColor() override;
|
||||
void parseUsername() override;
|
||||
void parseMessageID();
|
||||
void parseRoomID();
|
||||
void appendChannelName();
|
||||
void parseUsernameColor();
|
||||
void parseUsername();
|
||||
void appendUsername();
|
||||
void runIgnoreReplaces(
|
||||
std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes);
|
||||
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
||||
void parseHighlights();
|
||||
|
||||
boost::optional<EmotePtr> getTwitchBadge(const Badge &badge);
|
||||
void appendTwitchEmote(
|
||||
|
@ -71,8 +61,8 @@ private:
|
|||
void addWords(
|
||||
const QStringList &words,
|
||||
const std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes);
|
||||
void addTextOrEmoji(EmotePtr emote);
|
||||
void addTextOrEmoji(const QString &value);
|
||||
void addTextOrEmoji(EmotePtr emote) override;
|
||||
void addTextOrEmoji(const QString &value) override;
|
||||
|
||||
void appendTwitchBadges();
|
||||
void appendChatterinoBadges();
|
||||
|
@ -86,16 +76,7 @@ private:
|
|||
bool historicalMessage_ = false;
|
||||
|
||||
QString userId_;
|
||||
QColor usernameColor_;
|
||||
QString originalMessage_;
|
||||
bool senderIsBroadcaster{};
|
||||
|
||||
const bool action_ = false;
|
||||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
|
||||
QUrl highlightSoundUrl_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
Loading…
Reference in a new issue