Fix link parsing in IRC (#3334)
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
- Minor: Removed duplicate setting for toggling `Channel Point Redeemed Message` highlights (#3296)
|
- Minor: Removed duplicate setting for toggling `Channel Point Redeemed Message` highlights (#3296)
|
||||||
- Minor: Added support for opening channels from twitch.tv/popout links. (#3309)
|
- Minor: Added support for opening channels from twitch.tv/popout links. (#3309)
|
||||||
- Minor: Clean up chat messages of special line characters prior to sending. (#3312)
|
- Minor: Clean up chat messages of special line characters prior to sending. (#3312)
|
||||||
|
- Minor: IRC now parses/displays links like Twitch chat. (#3334)
|
||||||
- Minor: Added button & label for copying login name of user instead of display name in the user info popout. (#3335)
|
- Minor: Added button & label for copying login name of user instead of display name in the user info popout. (#3335)
|
||||||
- Bugfix: Fixed colored usernames sometimes not working. (#3170)
|
- Bugfix: Fixed colored usernames sometimes not working. (#3170)
|
||||||
- Bugfix: Restored ability to send duplicate `/me` messages. (#3166)
|
- Bugfix: Restored ability to send duplicate `/me` messages. (#3166)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "common/IrcColors.hpp"
|
|
||||||
#include "debug/Benchmark.hpp"
|
#include "debug/Benchmark.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
#include "messages/layouts/MessageLayoutContainer.hpp"
|
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||||
@@ -12,14 +11,6 @@
|
|||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
QRegularExpression IRC_COLOR_PARSE_REGEX(
|
|
||||||
"(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
|
|
||||||
QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
MessageElement::MessageElement(MessageElementFlags flags)
|
MessageElement::MessageElement(MessageElementFlags flags)
|
||||||
: flags_(flags)
|
: flags_(flags)
|
||||||
{
|
{
|
||||||
@@ -472,252 +463,6 @@ 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 = -1;
|
|
||||||
bg = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!match.captured(2).isEmpty())
|
|
||||||
{
|
|
||||||
fg = match.captured(2).toInt(nullptr);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fg = -1;
|
|
||||||
}
|
|
||||||
if (!match.captured(4).isEmpty())
|
|
||||||
{
|
|
||||||
bg = match.captured(4).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.horizontalAdvance(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The word does not fit on a new line, we need to wrap it
|
|
||||||
QString text = word.text;
|
|
||||||
std::vector<Segment> segments = word.segments;
|
|
||||||
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++)
|
|
||||||
{
|
|
||||||
if (!container.canAddElements())
|
|
||||||
{
|
|
||||||
// The container does not allow any more elements to be added, stop here
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto isSurrogate = text.size() > i + 1 &&
|
|
||||||
QChar::isHighSurrogate(text[i].unicode());
|
|
||||||
|
|
||||||
auto charWidth = isSurrogate
|
|
||||||
? metrics.horizontalAdvance(text.mid(i, 2))
|
|
||||||
: metrics.horizontalAdvance(text[i]);
|
|
||||||
|
|
||||||
if (!container.fitsInLine(width + charWidth))
|
|
||||||
{
|
|
||||||
std::vector<Segment> pieceSegments;
|
|
||||||
int charactersLeft = i - wordStart;
|
|
||||||
|
|
||||||
for (auto segmentIt = segments.begin();
|
|
||||||
segmentIt != segments.end();)
|
|
||||||
{
|
|
||||||
auto &segment = *segmentIt;
|
|
||||||
if (charactersLeft >= segment.text.length())
|
|
||||||
{
|
|
||||||
// Entire segment fits in this piece
|
|
||||||
pieceSegments.push_back(segment);
|
|
||||||
charactersLeft -= segment.text.length();
|
|
||||||
segmentIt = segments.erase(segmentIt);
|
|
||||||
|
|
||||||
assert(charactersLeft >= 0);
|
|
||||||
|
|
||||||
if (charactersLeft == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Only part of the segment fits in this piece
|
|
||||||
// We create a new segment with the characters that fit, and modify the segment we checked to only contain the characters we didn't consume
|
|
||||||
Segment segmentThatFitsInPiece{
|
|
||||||
segment.text.left(charactersLeft), segment.fg,
|
|
||||||
segment.bg};
|
|
||||||
pieceSegments.emplace_back(segmentThatFitsInPiece);
|
|
||||||
segment.text = segment.text.mid(charactersLeft);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.addElementNoLineBreak(
|
|
||||||
getTextLayoutElement(text.mid(wordStart, i - wordStart),
|
|
||||||
pieceSegments, width, false));
|
|
||||||
container.breakLine();
|
|
||||||
|
|
||||||
wordStart = i;
|
|
||||||
width = charWidth;
|
|
||||||
|
|
||||||
if (isSurrogate)
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
width += charWidth;
|
|
||||||
|
|
||||||
if (isSurrogate)
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add last remaining text & segments
|
|
||||||
container.addElementNoLineBreak(
|
|
||||||
getTextLayoutElement(text.mid(wordStart), segments, width,
|
|
||||||
this->hasTrailingSpace()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LinebreakElement::LinebreakElement(MessageElementFlags flags)
|
LinebreakElement::LinebreakElement(MessageElementFlags flags)
|
||||||
: MessageElement(flags)
|
: MessageElement(flags)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -333,36 +333,6 @@ public:
|
|||||||
MessageElementFlags flags) override;
|
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_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Forces a linebreak
|
// Forces a linebreak
|
||||||
class LinebreakElement : public MessageElement
|
class LinebreakElement : public MessageElement
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -405,40 +405,4 @@ 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_)
|
|
||||||
{
|
|
||||||
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.horizontalAdvance(segment.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -140,25 +140,4 @@ private:
|
|||||||
QString line2;
|
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
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "providers/irc/IrcMessageBuilder.hpp"
|
#include "providers/irc/IrcMessageBuilder.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
|
#include "common/IrcColors.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||||
@@ -15,6 +16,14 @@
|
|||||||
#include "util/IrcHelpers.hpp"
|
#include "util/IrcHelpers.hpp"
|
||||||
#include "widgets/Window.hpp"
|
#include "widgets/Window.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QRegularExpression IRC_COLOR_PARSE_REGEX(
|
||||||
|
"(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
|
||||||
|
QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
IrcMessageBuilder::IrcMessageBuilder(
|
IrcMessageBuilder::IrcMessageBuilder(
|
||||||
@@ -68,7 +77,107 @@ MessagePtr IrcMessageBuilder::build()
|
|||||||
|
|
||||||
void IrcMessageBuilder::addWords(const QStringList &words)
|
void IrcMessageBuilder::addWords(const QStringList &words)
|
||||||
{
|
{
|
||||||
this->emplace<IrcTextElement>(words.join(' '), MessageElementFlag::Text);
|
MessageColor defaultColorType = MessageColor::Text;
|
||||||
|
auto defaultColor = defaultColorType.getColor(*getApp()->themes);
|
||||||
|
QColor textColor = defaultColor;
|
||||||
|
int fg = -1;
|
||||||
|
int bg = -1;
|
||||||
|
|
||||||
|
for (auto word : words)
|
||||||
|
{
|
||||||
|
if (word.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto string = QString(word);
|
||||||
|
|
||||||
|
// Actually just text
|
||||||
|
auto linkString = this->matchLink(string);
|
||||||
|
auto link = Link();
|
||||||
|
|
||||||
|
if (!linkString.isEmpty())
|
||||||
|
{
|
||||||
|
this->addLink(string, linkString);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the word contain a color changer? If so, split on it.
|
||||||
|
// Add color indicators, then combine into the same word with the color being changed
|
||||||
|
|
||||||
|
auto i = IRC_COLOR_PARSE_REGEX.globalMatch(string);
|
||||||
|
|
||||||
|
if (!i.hasNext())
|
||||||
|
{
|
||||||
|
this->emplace<TextElement>(string, MessageElementFlag::Text,
|
||||||
|
textColor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
int lastPos = 0;
|
||||||
|
|
||||||
|
while (i.hasNext())
|
||||||
|
{
|
||||||
|
auto match = i.next();
|
||||||
|
|
||||||
|
if (lastPos != match.capturedStart() && match.capturedStart() != 0)
|
||||||
|
{
|
||||||
|
if (fg >= 0 && fg <= 98)
|
||||||
|
{
|
||||||
|
textColor = IRC_COLORS[fg];
|
||||||
|
getApp()->themes->normalizeColor(textColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textColor = defaultColor;
|
||||||
|
}
|
||||||
|
this->emplace<TextElement>(
|
||||||
|
string.mid(lastPos, match.capturedStart() - lastPos),
|
||||||
|
MessageElementFlag::Text, textColor)
|
||||||
|
->setTrailingSpace(false);
|
||||||
|
lastPos = match.capturedStart() + match.capturedLength();
|
||||||
|
}
|
||||||
|
if (!match.captured(1).isEmpty())
|
||||||
|
{
|
||||||
|
fg = -1;
|
||||||
|
bg = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match.captured(2).isEmpty())
|
||||||
|
{
|
||||||
|
fg = match.captured(2).toInt(nullptr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fg = -1;
|
||||||
|
}
|
||||||
|
if (!match.captured(4).isEmpty())
|
||||||
|
{
|
||||||
|
bg = match.captured(4).toInt(nullptr);
|
||||||
|
}
|
||||||
|
else if (fg == -1)
|
||||||
|
{
|
||||||
|
bg = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPos = match.capturedStart() + match.capturedLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fg >= 0 && fg <= 98)
|
||||||
|
{
|
||||||
|
textColor = IRC_COLORS[fg];
|
||||||
|
getApp()->themes->normalizeColor(textColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textColor = defaultColor;
|
||||||
|
}
|
||||||
|
this->emplace<TextElement>(string.mid(lastPos),
|
||||||
|
MessageElementFlag::Text, textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->message().elements.back()->setTrailingSpace(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrcMessageBuilder::appendUsername()
|
void IrcMessageBuilder::appendUsername()
|
||||||
|
|||||||
Reference in New Issue
Block a user