Merge branch 'master' into irc-support

This commit is contained in:
fourtf 2019-09-18 13:03:16 +02:00
commit 3ab7362304
204 changed files with 17026 additions and 16467 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize line endings to LF for files that Git detects as text
* text=auto

7
.gitmodules vendored
View file

@ -4,10 +4,6 @@
[submodule "lib/humanize"]
path = lib/humanize
url = https://github.com/pajlada/humanize.git
[submodule "lib/websocketpp"]
path = lib/websocketpp
url = https://github.com/zaphoyd/websocketpp.git
branch = develop
[submodule "lib/qBreakpad"]
path = lib/qBreakpad
url = https://github.com/jiakuan/qBreakpad.git
@ -29,3 +25,6 @@
[submodule "lib/qtkeychain"]
path = lib/qtkeychain
url = https://github.com/Chatterino/qtkeychain
[submodule "lib/websocketpp"]
path = lib/websocketpp
url = https://github.com/ziocleto/websocketpp

View file

@ -40,6 +40,8 @@ build_script:
cp release/chatterino.exe Chatterino2/
echo nightly > Chatterino2/modes
7z a chatterino-windows-x86-64.zip Chatterino2/
artifacts:
- path: build/chatterino-windows-x86-64.zip
@ -48,7 +50,7 @@ deploy:
- provider: GitHub
tag: nightly-build
release: nightly-build
description: 'nightly v$(appveyor_build_version) built $(APPVEYOR_REPO_COMMIT_TIMESTAMP)\nLast change: $(APPVEYOR_REPO_COMMIT_MESSAGE) \n$(APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED)'
description: 'nightly v$(appveyor_build_version) built 👉 $(APPVEYOR_REPO_COMMIT_TIMESTAMP) 👈\nLast change: $(APPVEYOR_REPO_COMMIT_MESSAGE) \n$(APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED)'
auth_token:
secure: sAJzAbiQSsYZLT+byDar9u61X0E9o35anaPMSFkOzdHeDFHjx1kW4cDP/4EEbxhx
repository: Chatterino/chatterino2

View file

@ -83,6 +83,7 @@ SOURCES += \
src/common/DownloadManager.cpp \
src/common/Env.cpp \
src/common/LinkParser.cpp \
src/common/Modes.cpp \
src/common/NetworkManager.cpp \
src/common/NetworkPrivate.cpp \
src/common/NetworkRequest.cpp \
@ -123,12 +124,14 @@ SOURCES += \
src/messages/MessageColor.cpp \
src/messages/MessageContainer.cpp \
src/messages/MessageElement.cpp \
src/messages/search/AuthorPredicate.cpp \
src/messages/search/LinkPredicate.cpp \
src/messages/search/SubstringPredicate.cpp \
src/providers/bttv/BttvEmotes.cpp \
src/providers/bttv/LoadBttvChannelEmote.cpp \
src/providers/chatterino/ChatterinoBadges.cpp \
src/providers/emoji/Emojis.cpp \
src/providers/ffz/FfzEmotes.cpp \
src/providers/ffz/FfzModBadge.cpp \
src/providers/irc/AbstractIrcServer.cpp \
src/providers/irc/Irc2.cpp \
src/providers/irc/IrcAccount.cpp \
@ -239,6 +242,7 @@ HEADERS += \
src/common/DownloadManager.hpp \
src/common/Env.hpp \
src/common/LinkParser.hpp \
src/common/Modes.hpp \
src/common/NetworkCommon.hpp \
src/common/NetworkManager.hpp \
src/common/NetworkPrivate.hpp \
@ -276,6 +280,7 @@ HEADERS += \
src/controllers/taggedusers/TaggedUser.hpp \
src/controllers/taggedusers/TaggedUsersController.hpp \
src/controllers/taggedusers/TaggedUsersModel.hpp \
src/ForwardDecl.hpp \
src/messages/Emote.hpp \
src/messages/Image.hpp \
src/messages/ImageSet.hpp \
@ -291,6 +296,10 @@ HEADERS += \
src/messages/MessageContainer.hpp \
src/messages/MessageElement.hpp \
src/messages/MessageParseArgs.hpp \
src/messages/search/AuthorPredicate.hpp \
src/messages/search/LinkPredicate.hpp \
src/messages/search/MessagePredicate.hpp \
src/messages/search/SubstringPredicate.hpp \
src/messages/Selection.hpp \
src/PrecompiledHeader.hpp \
src/providers/bttv/BttvEmotes.hpp \
@ -298,7 +307,6 @@ HEADERS += \
src/providers/chatterino/ChatterinoBadges.hpp \
src/providers/emoji/Emojis.hpp \
src/providers/ffz/FfzEmotes.hpp \
src/providers/ffz/FfzModBadge.hpp \
src/providers/irc/AbstractIrcServer.hpp \
src/providers/irc/Irc2.hpp \
src/providers/irc/IrcAccount.hpp \
@ -356,6 +364,7 @@ HEADERS += \
src/util/rangealgorithm.hpp \
src/util/RapidjsonHelpers.hpp \
src/util/RemoveScrollAreaBackground.hpp \
src/util/SampleCheerMessages.hpp \
src/util/SharedPtrElementLess.hpp \
src/util/StandardItemHelper.hpp \
src/util/StreamLink.hpp \

View file

@ -44,12 +44,12 @@
namespace AB_NAMESPACE {
BaseWindow::BaseWindow(QWidget *parent, Flags _flags)
BaseWindow::BaseWindow(FlagsEnum<Flags> _flags, QWidget *parent)
: BaseWidget(parent,
Qt::Window | ((_flags & TopMost) ? Qt::WindowStaysOnTopHint
Qt::Window | (_flags.has(TopMost) ? Qt::WindowStaysOnTopHint
: Qt::WindowFlags()))
, enableCustomFrame_(_flags & EnableCustomFrame)
, frameless_(_flags & Frameless)
, enableCustomFrame_(_flags.has(EnableCustomFrame))
, frameless_(_flags.has(Frameless))
, flags_(_flags)
{
if (this->frameless_)
@ -74,9 +74,13 @@ BaseWindow::BaseWindow(QWidget *parent, Flags _flags)
createWindowShortcut(this, "CTRL+0",
[] { getSettings()->uiScale.setValue(1); });
// QTimer::this->scaleChangedEvent(this->getScale());
this->resize(300, 150);
#ifdef USEWINSDK
this->useNextBounds_.setSingleShot(true);
QObject::connect(&this->useNextBounds_, &QTimer::timeout, this,
[this]() { this->currentBounds_ = this->nextBounds_; });
#endif
}
void BaseWindow::setInitialBounds(const QRect &bounds)
@ -107,11 +111,6 @@ float BaseWindow::qtFontScale() const
return this->scale() / this->nativeScale_;
}
BaseWindow::Flags BaseWindow::getFlags()
{
return this->flags_;
}
void BaseWindow::init()
{
this->setWindowIcon(QIcon(":/images/icon.png"));
@ -200,7 +199,7 @@ void BaseWindow::init()
#ifdef USEWINSDK
// fourtf: don't ask me why we need to delay this
if (!(this->flags_ & Flags::TopMost))
if (!this->flags_.has(TopMost))
{
QTimer::singleShot(1, this, [this] {
getSettings()->windowTopMost.connect(
@ -364,7 +363,7 @@ void BaseWindow::onFocusLost()
void BaseWindow::mousePressEvent(QMouseEvent *event)
{
#ifndef Q_OS_WIN
if (this->flags_ & FramelessDraggable)
if (this->flags_.has(FramelessDraggable))
{
this->movingRelativePos = event->localPos();
if (auto widget =
@ -400,7 +399,7 @@ void BaseWindow::mousePressEvent(QMouseEvent *event)
void BaseWindow::mouseReleaseEvent(QMouseEvent *event)
{
#ifndef Q_OS_WIN
if (this->flags_ & FramelessDraggable)
if (this->flags_.has(FramelessDraggable))
{
if (this->moving)
{
@ -416,7 +415,7 @@ void BaseWindow::mouseReleaseEvent(QMouseEvent *event)
void BaseWindow::mouseMoveEvent(QMouseEvent *event)
{
#ifndef Q_OS_WIN
if (this->flags_ & FramelessDraggable)
if (this->flags_.has(FramelessDraggable))
{
if (this->moving)
{
@ -515,7 +514,7 @@ void BaseWindow::resizeEvent(QResizeEvent *)
getApp()->windows->queueSave();
#endif
this->moveIntoDesktopRect(this);
//this->moveIntoDesktopRect(this);
this->calcButtonsSizes();
}
@ -535,6 +534,18 @@ void BaseWindow::closeEvent(QCloseEvent *)
this->closing.invoke();
}
void BaseWindow::showEvent(QShowEvent *)
{
if (this->frameless_)
{
this->moveIntoDesktopRect(this);
qDebug() << "show";
QTimer::singleShot(30, this,
[this] { this->moveIntoDesktopRect(this); });
}
}
void BaseWindow::moveIntoDesktopRect(QWidget *parent)
{
if (!this->stayInScreenRect_)
@ -643,7 +654,7 @@ void BaseWindow::paintEvent(QPaintEvent *)
void BaseWindow::updateScale()
{
auto scale =
this->nativeScale_ * (this->flags_ & DisableCustomScaling
this->nativeScale_ * (this->flags_.has(DisableCustomScaling)
? 1
: getABSettings()->getClampedUiScale());
@ -702,17 +713,11 @@ bool BaseWindow::handleDPICHANGED(MSG *msg)
float _scale = dpi / 96.f;
static bool firstResize = true;
if (!firstResize)
{
auto *prcNewWindow = reinterpret_cast<RECT *>(msg->lParam);
SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top,
prcNewWindow->right - prcNewWindow->left,
prcNewWindow->bottom - prcNewWindow->top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
firstResize = false;
this->nativeScale_ = _scale;
this->updateScale();
@ -809,8 +814,10 @@ bool BaseWindow::handleSIZE(MSG *msg)
{
this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0);
}
if ((this->isNotMinimizedOrMaximized_ =
msg->wParam == SIZE_RESTORED))
this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED;
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
@ -818,6 +825,7 @@ bool BaseWindow::handleSIZE(MSG *msg)
QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
}
this->useNextBounds_.stop();
}
}
return false;
@ -833,8 +841,10 @@ bool BaseWindow::handleMOVE(MSG *msg)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->currentBounds_ = QRect(QPoint(rect.left, rect.top),
this->nextBounds_ = QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->useNextBounds_.start(10);
}
#endif
return false;
@ -942,7 +952,7 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
return true;
}
else if (this->flags_ & FramelessDraggable)
else if (this->flags_.has(FramelessDraggable))
{
*result = 0;
bool client = false;

View file

@ -4,6 +4,7 @@
#include <functional>
#include <pajlada/signals/signalholder.hpp>
#include "common/FlagsEnum.hpp"
class QHBoxLayout;
struct tagMSG;
@ -32,7 +33,8 @@ public:
enum ActionOnFocusLoss { Nothing, Delete, Close, Hide };
explicit BaseWindow(QWidget *parent = nullptr, Flags flags_ = None);
explicit BaseWindow(FlagsEnum<Flags> flags_ = None,
QWidget *parent = nullptr);
void setInitialBounds(const QRect &bounds);
QRect getBounds();
@ -54,8 +56,6 @@ public:
virtual float scale() const override;
float qtFontScale() const;
Flags getFlags();
pajlada::Signals::NoArgSignal closing;
static bool supportsCustomWindowFrame();
@ -72,6 +72,7 @@ protected:
virtual void resizeEvent(QResizeEvent *) override;
virtual void moveEvent(QMoveEvent *) override;
virtual void closeEvent(QCloseEvent *) override;
virtual void showEvent(QShowEvent *) override;
virtual void themeChangedEvent() override;
virtual bool event(QEvent *event) override;
@ -106,7 +107,7 @@ private:
bool frameless_;
bool stayInScreenRect_ = false;
bool shown_ = false;
Flags flags_;
FlagsEnum<Flags> flags_;
float nativeScale_ = 1;
struct {
@ -123,6 +124,8 @@ private:
#ifdef USEWINSDK
QRect initalBounds_;
QRect currentBounds_;
QRect nextBounds_;
QTimer useNextBounds_;
bool isNotMinimizedOrMaximized_{};
#endif

View file

@ -21,7 +21,7 @@ TooltipWidget *TooltipWidget::getInstance()
}
TooltipWidget::TooltipWidget(BaseWidget *parent)
: BaseWindow(parent, BaseWindow::TopMost)
: BaseWindow(BaseWindow::TopMost, parent)
, displayImage_(new QLabel())
, displayText_(new QLabel())
{

@ -1 +1 @@
Subproject commit 19cad9925f83d15d7487c16f0491f4741ec9f674
Subproject commit 1e0138c7ccedc6be859d28270ccd6195f235a94e

10
src/ForwardDecl.hpp Normal file
View file

@ -0,0 +1,10 @@
#pragma once
namespace chatterino {
class Channel;
class ChannelView;
using ChannelPtr = std::shared_ptr<Channel>;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
} // namespace chatterino

View file

@ -1,3 +1,5 @@
#pragma once
#ifdef __cplusplus
# include <fmt/format.h>
# include <irccommand.h>

36
src/common/Modes.cpp Normal file
View file

@ -0,0 +1,36 @@
#include "Modes.hpp"
#include "util/CombinePath.hpp"
#include <QCoreApplication>
namespace chatterino {
Modes::Modes()
{
QFile file(combinePath(QCoreApplication::applicationDirPath(), "modes"));
file.open(QIODevice::ReadOnly);
while (!file.atEnd())
{
auto line = QString(file.readLine()).trimmed();
// we need to know if it is a nightly build to disable updates on windows
if (line == "nightly")
{
this->isNightly = true;
}
else if (line == "portable")
{
this->isPortable = true;
}
}
}
const Modes &Modes::getInstance()
{
static Modes instance;
return instance;
}
} // namespace chatterino

16
src/common/Modes.hpp Normal file
View file

@ -0,0 +1,16 @@
#pragma once
namespace chatterino {
class Modes
{
public:
Modes();
static const Modes &getInstance();
bool isNightly{};
bool isPortable{};
};
} // namespace chatterino

View file

@ -32,6 +32,10 @@ NetworkData::~NetworkData()
QString NetworkData::getHash()
{
static std::mutex mu;
std::lock_guard lock(mu);
if (this->hash_.isEmpty())
{
QByteArray bytes;
@ -242,7 +246,6 @@ void load(const std::shared_ptr<NetworkData> &data)
if (data->cache_)
{
QtConcurrent::run(loadCached, data);
loadCached(data);
}
else
{

View file

@ -2,7 +2,7 @@
#include <QtGlobal>
#define CHATTERINO_VERSION "2.1.4-beta-1"
#define CHATTERINO_VERSION "2.1.4"
#if defined(Q_OS_WIN)
# define CHATTERINO_OS "win"

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel
HighlightModel::HighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(4, parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
{
}
@ -16,12 +16,13 @@ HighlightModel::HighlightModel(QObject *parent)
HighlightPhrase HighlightModel::getItemFromRow(
std::vector<QStandardItem *> &row, const HighlightPhrase &original)
{
// key, alert, sound, regex
// key, alert, sound, regex, case-sensitivity
return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::CheckStateRole).toBool(),
row[2]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool()};
row[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
}
// turns a row in the model into a vector item
@ -32,6 +33,7 @@ void HighlightModel::getRowFromItem(const HighlightPhrase &item,
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
}
void HighlightModel::afterInit()

View file

@ -15,21 +15,24 @@ public:
bool operator==(const HighlightPhrase &other) const
{
return std::tie(this->pattern_, this->sound_, this->alert_,
this->isRegex_) == std::tie(other.pattern_,
other.sound_, other.alert_,
other.isRegex_);
this->isRegex_, this->caseSensitive_) ==
std::tie(other.pattern_, other.sound_, other.alert_,
other.isRegex_, other.caseSensitive_);
}
HighlightPhrase(const QString &pattern, bool alert, bool sound,
bool isRegex)
bool isRegex, bool caseSensitive)
: pattern_(pattern)
, alert_(alert)
, sound_(sound)
, isRegex_(isRegex)
, regex_(isRegex_ ? pattern
, caseSensitive_(caseSensitive)
, regex_(
isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::CaseInsensitiveOption |
QRegularExpression::UseUnicodePropertiesOption)
QRegularExpression::UseUnicodePropertiesOption |
(caseSensitive_ ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption))
{
}
@ -60,11 +63,17 @@ public:
return this->isValid() && this->regex_.match(subject).hasMatch();
}
bool isCaseSensitive() const
{
return this->caseSensitive_;
}
private:
QString pattern_;
bool alert_;
bool sound_;
bool isRegex_;
bool caseSensitive_;
QRegularExpression regex_;
};
} // namespace chatterino
@ -82,6 +91,7 @@ struct Serialize<chatterino::HighlightPhrase> {
chatterino::rj::set(ret, "alert", value.getAlert(), a);
chatterino::rj::set(ret, "sound", value.getSound(), a);
chatterino::rj::set(ret, "regex", value.isRegex(), a);
chatterino::rj::set(ret, "case", value.isCaseSensitive(), a);
return ret;
}
@ -93,20 +103,24 @@ struct Deserialize<chatterino::HighlightPhrase> {
{
if (!value.IsObject())
{
return chatterino::HighlightPhrase(QString(), true, false, false);
return chatterino::HighlightPhrase(QString(), true, false, false,
false);
}
QString _pattern;
bool _alert = true;
bool _sound = false;
bool _isRegex = false;
bool _caseSensitive = false;
chatterino::rj::getSafe(value, "pattern", _pattern);
chatterino::rj::getSafe(value, "alert", _alert);
chatterino::rj::getSafe(value, "sound", _sound);
chatterino::rj::getSafe(value, "regex", _isRegex);
chatterino::rj::getSafe(value, "case", _caseSensitive);
return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex);
return chatterino::HighlightPhrase(_pattern, _alert, _sound,
_isRegex, _caseSensitive);
}
};

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel
UserHighlightModel::UserHighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(4, parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
{
}
@ -21,7 +21,8 @@ HighlightPhrase UserHighlightModel::getItemFromRow(
return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::CheckStateRole).toBool(),
row[2]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool()};
row[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
}
// row into vector item
@ -32,6 +33,7 @@ void UserHighlightModel::getRowFromItem(const HighlightPhrase &item,
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
}
} // namespace chatterino

View file

@ -22,7 +22,7 @@ public:
{
}
std::size_t size()
std::size_t size() const
{
return this->length_;
}

View file

@ -145,7 +145,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
QSize(int(container.getScale() * image->width() * emoteScale),
int(container.getScale() * image->height() * emoteScale));
container.addElement((new ImageLayoutElement(*this, image, size))
container.addElement(this->makeImageLayoutElement(image, size)
->setLink(this->getLink()));
}
else
@ -159,6 +159,27 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
}
}
MessageLayoutElement *EmoteElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
return new ImageLayoutElement(*this, image, size);
}
// MOD BADGE
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
: EmoteElement(data, flags_)
{
}
MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
static const QColor modBadgeBackgroundColor("#34AE0A");
return new ImageWithBackgroundLayoutElement(*this, image, size,
modBadgeBackgroundColor);
}
// BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags)

View file

@ -17,6 +17,7 @@
namespace chatterino {
class Channel;
struct MessageLayoutContainer;
class MessageLayoutElement;
class Image;
using ImagePtr = std::shared_ptr<Image>;
@ -209,11 +210,26 @@ public:
MessageElementFlags flags_) override;
EmotePtr getEmote() const;
protected:
virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
const QSize &size);
private:
std::unique_ptr<TextElement> textElement_;
EmotePtr emote_;
};
// Behaves like an emote element, except it creates a different image layout element that draws the mod badge background
class ModBadgeElement : public EmoteElement
{
public:
ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
protected:
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
const QSize &size) override;
};
class BadgeElement : public MessageElement
{
public:

View file

@ -168,6 +168,33 @@ int ImageLayoutElement::getXFromIndex(int index)
}
}
//
// IMAGE WITH BACKGROUND
//
ImageWithBackgroundLayoutElement::ImageWithBackgroundLayoutElement(
MessageElement &creator, ImagePtr image, const QSize &size, QColor color)
: ImageLayoutElement(creator, image, size)
, color_(color)
{
}
void ImageWithBackgroundLayoutElement::paint(QPainter &painter)
{
if (this->image_ == nullptr)
{
return;
}
auto pixmap = this->image_->pixmapOrLoad();
if (pixmap && !this->image_->animated())
{
painter.fillRect(QRectF(this->getRect()), this->color_);
// fourtf: make it use qreal values
painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF());
}
}
//
// TEXT
//

View file

@ -72,10 +72,22 @@ protected:
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
private:
ImagePtr image_;
};
class ImageWithBackgroundLayoutElement : public ImageLayoutElement
{
public:
ImageWithBackgroundLayoutElement(MessageElement &creator, ImagePtr image,
const QSize &size, QColor color);
protected:
void paint(QPainter &painter) override;
private:
QColor color_;
};
// TEXT
class TextLayoutElement : public MessageLayoutElement
{

View file

@ -0,0 +1,24 @@
#include "messages/search/AuthorPredicate.hpp"
namespace chatterino {
AuthorPredicate::AuthorPredicate(const QStringList &authors)
: authors_()
{
// Check if any comma-seperated values were passed and transform those
for (const auto &entry : authors)
{
for (const auto &author : entry.split(',', QString::SkipEmptyParts))
{
this->authors_ << author;
}
}
}
bool AuthorPredicate::appliesTo(const Message &message)
{
return authors_.contains(message.displayName, Qt::CaseInsensitive) ||
authors_.contains(message.loginName, Qt::CaseInsensitive);
}
} // namespace chatterino

View file

@ -0,0 +1,38 @@
#pragma once
#include "messages/search/MessagePredicate.hpp"
namespace chatterino {
/**
* @brief MessagePredicate checking for the author/sender of a message.
*
* This predicate will only allow messages that are sent by a list of users,
* specified by their user names.
*/
class AuthorPredicate : public MessagePredicate
{
public:
/**
* @brief Create an AuthorPredicate with a list of users to search for.
*
* @param authors a list of user names that a message should be sent from
*/
AuthorPredicate(const QStringList &authors);
/**
* @brief Checks whether the message is authored by any of the users passed
* in the constructor.
*
* @param message the message to check
* @return true if the message was authored by one of the specified users,
* false otherwise
*/
bool appliesTo(const Message &message);
private:
/// Holds the user names that will be searched for
QStringList authors_;
};
} // namespace chatterino

View file

@ -0,0 +1,22 @@
#include "messages/search/LinkPredicate.hpp"
#include "common/LinkParser.hpp"
namespace chatterino {
LinkPredicate::LinkPredicate()
{
}
bool LinkPredicate::appliesTo(const Message &message)
{
for (const auto &word :
message.messageText.split(' ', QString::SkipEmptyParts))
{
if (LinkParser(word).hasMatch())
return true;
}
return false;
}
} // namespace chatterino

View file

@ -0,0 +1,26 @@
#pragma once
#include "messages/search/MessagePredicate.hpp"
namespace chatterino {
/**
* @brief MessagePredicate checking whether a link exists in the message.
*
* This predicate will only allow messages that contain a link.
*/
class LinkPredicate : public MessagePredicate
{
public:
LinkPredicate();
/**
* @brief Checks whether the message contains a link.
*
* @param message the message to check
* @return true if the message contains a link, false otherwise
*/
bool appliesTo(const Message &message);
};
} // namespace chatterino

View file

@ -0,0 +1,31 @@
#pragma once
#include "messages/Message.hpp"
#include <memory>
namespace chatterino {
/**
* @brief Abstract base class for message predicates.
*
* Message predicates define certain features a message can satisfy.
* Features are represented by classes derived from this abstract class.
* A derived class must override `appliesTo` in order to test for the desired
* feature.
*/
class MessagePredicate
{
public:
/**
* @brief Checks whether this predicate applies to the passed message.
*
* Implementations of `appliesTo` should never change the message's content
* in order to be compatible with other MessagePredicates.
*
* @param message the message to check for this predicate
* @return true if this predicate applies, false otherwise
*/
virtual bool appliesTo(const Message &message) = 0;
};
} // namespace chatterino

View file

@ -0,0 +1,15 @@
#include "messages/search/SubstringPredicate.hpp"
namespace chatterino {
SubstringPredicate::SubstringPredicate(const QString &search)
: search_(search)
{
}
bool SubstringPredicate::appliesTo(const Message &message)
{
return message.searchText.contains(this->search_, Qt::CaseInsensitive);
}
} // namespace chatterino

View file

@ -0,0 +1,41 @@
#pragma once
#include "messages/search/MessagePredicate.hpp"
namespace chatterino {
/**
* @brief MessagePredicate checking whether a substring exists in the message.
*
* This predicate will only allow messages that contain a certain substring in
* their `searchText`.
*/
class SubstringPredicate : public MessagePredicate
{
public:
/**
* @brief Create a SubstringPredicate with a substring to search for.
*
* The passed string is searched for case-insensitively.
*
* @param search the string to search for in the message
*/
SubstringPredicate(const QString &search);
/**
* @brief Checks whether the message contains the substring passed in the
* constructor.
*
* The check is done case-insensitively.
*
* @param message the message to check
* @return true if the message contains the substring, false otherwise
*/
bool appliesTo(const Message &message);
private:
/// Holds the substring to search for in a message's `messageText`
const QString search_;
};
} // namespace chatterino

View file

@ -79,7 +79,38 @@ namespace {
return {Success, std::move(emotes)};
}
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
boost::optional<EmotePtr> parseModBadge(const QJsonObject &jsonRoot)
{
boost::optional<EmotePtr> modBadge;
auto room = jsonRoot.value("room").toObject();
auto modUrls = room.value("mod_urls").toObject();
if (!modUrls.isEmpty())
{
auto modBadge1x = getEmoteLink(modUrls, "1");
auto modBadge2x = getEmoteLink(modUrls, "2");
auto modBadge3x = getEmoteLink(modUrls, "4");
auto modBadgeImageSet = ImageSet{
Image::fromUrl(modBadge1x, 1),
modBadge2x.string.isEmpty() ? Image::getEmpty()
: Image::fromUrl(modBadge2x, 0.5),
modBadge3x.string.isEmpty() ? Image::getEmpty()
: Image::fromUrl(modBadge3x, 0.25),
};
modBadge = std::make_shared<Emote>(Emote{
{""},
modBadgeImageSet,
Tooltip{"Twitch Channel Moderator"},
modBadge1x,
});
}
return modBadge;
}
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
{
auto jsonSets = jsonRoot.value("sets").toObject();
auto emotes = EmoteMap();
@ -110,7 +141,7 @@ namespace {
}
}
return {Success, std::move(emotes)};
return emotes;
}
} // namespace
@ -151,19 +182,26 @@ void FfzEmotes::loadEmotes()
.execute();
}
void FfzEmotes::loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback)
void FfzEmotes::loadChannel(
const QString &channelId, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback)
{
log("[FFZEmotes] Reload FFZ Channel Emotes for channel {}\n", channelId);
NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId)
.timeout(20000)
.onSuccess([callback = std::move(callback)](auto result) -> Outcome {
auto pair = parseChannelEmotes(result.parseJson());
if (pair.first)
callback(std::move(pair.second));
return pair.first;
.onSuccess([emoteCallback = std::move(emoteCallback),
modBadgeCallback =
std::move(modBadgeCallback)](auto result) -> Outcome {
auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json);
auto modBadge = parseModBadge(json);
emoteCallback(std::move(emoteMap));
modBadgeCallback(std::move(modBadge));
return Success;
})
.onError([channelId](int result) {
if (result == 203)

View file

@ -22,8 +22,10 @@ public:
std::shared_ptr<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
static void loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback);
static void loadChannel(
const QString &channelId,
std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;

View file

@ -1,63 +0,0 @@
#include "FfzModBadge.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QJsonObject>
#include <QPainter>
#include <QString>
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "messages/Emote.hpp"
namespace chatterino {
FfzModBadge::FfzModBadge(const QString &channelName)
: channelName_(channelName)
{
}
void FfzModBadge::loadCustomModBadge()
{
static QString partialUrl("https://cdn.frankerfacez.com/room-badge/mod/");
QString url = partialUrl + channelName_ + "/1";
NetworkRequest(url)
.onSuccess([this, url](auto result) -> Outcome {
auto data = result.getData();
QBuffer buffer(const_cast<QByteArray *>(&data));
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
if (reader.imageCount() == 0)
return Failure;
QPixmap badgeOverlay = QPixmap::fromImageReader(&reader);
QPixmap badgePixmap(18, 18);
// the default mod badge green color
badgePixmap.fill(QColor("#34AE0A"));
QPainter painter(&badgePixmap);
QRectF rect(0, 0, 18, 18);
painter.drawPixmap(rect, badgeOverlay, rect);
auto emote = Emote{{""},
ImageSet{Image::fromPixmap(badgePixmap)},
Tooltip{"Twitch Channel Moderator"},
Url{url}};
this->badge_ = std::make_shared<Emote>(emote);
// getBadge.execute();
return Success;
})
.execute();
}
EmotePtr FfzModBadge::badge() const
{
return this->badge_;
}
} // namespace chatterino

View file

@ -1,25 +0,0 @@
#pragma once
#include <QString>
#include <boost/optional.hpp>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class FfzModBadge
{
public:
FfzModBadge(const QString &channelName);
void loadCustomModBadge();
EmotePtr badge() const;
private:
const QString channelName_;
EmotePtr badge_;
};
} // namespace chatterino

View file

@ -48,8 +48,14 @@ void ChatroomChannel::refreshFFZChannelEmotes()
{
return;
}
FfzEmotes::loadChannel(this->chatroomOwnerId, [this](auto &&emoteMap) {
this->ffzEmotes_.set(std::make_shared<EmoteMap>(std::move(emoteMap)));
FfzEmotes::loadChannel(
this->chatroomOwnerId,
[this](auto &&emoteMap) {
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
},
[this](auto &&modBadge) {
this->ffzCustomModBadge_.set(std::move(modBadge));
});
}

View file

@ -51,8 +51,8 @@ public:
private:
void addMessage(Communi::IrcMessage *message, const QString &target,
const QString &content, TwitchIrcServer &server, bool isResub,
bool isAction);
const QString &content, TwitchIrcServer &server,
bool isResub, bool isAction);
};
} // namespace chatterino

View file

@ -88,7 +88,6 @@ TwitchChannel::TwitchChannel(const QString &name,
, globalFfz_(ffz)
, bttvEmotes_(std::make_shared<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, ffzCustomModBadge_(name)
, mod_(false)
{
log("[TwitchChannel:{}] Opened", name);
@ -139,7 +138,6 @@ void TwitchChannel::initialize()
{
this->refreshChatters();
this->refreshBadges();
this->ffzCustomModBadge_.loadCustomModBadge();
}
bool TwitchChannel::isEmpty() const
@ -165,10 +163,17 @@ void TwitchChannel::refreshBTTVChannelEmotes()
void TwitchChannel::refreshFFZChannelEmotes()
{
FfzEmotes::loadChannel(
this->roomId(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
this->roomId(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
if (auto shared = weak.lock())
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
},
[this, weak = weakOf<Channel>(this)](auto &&modBadge) {
if (auto shared = weak.lock())
{
this->ffzCustomModBadge_.set(std::move(modBadge));
}
});
}
@ -685,22 +690,23 @@ void TwitchChannel::refreshBadges()
void TwitchChannel::refreshCheerEmotes()
{
/*auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" +
this->getRoomId()};
auto request = NetworkRequest::twitchRequest(url.string);
request.setCaller(QThread::currentThread());
request.onSuccess(
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
QString url("https://api.twitch.tv/kraken/bits/actions?channel_id=" +
this->roomId());
NetworkRequest::twitchRequest(url)
.onSuccess([this,
weak = weakOf<Channel>(this)](auto result) -> Outcome {
auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson());
std::vector<CheerEmoteSet> emoteSets;
for (auto &set : cheerEmoteSets) {
for (auto &set : cheerEmoteSets)
{
auto cheerEmoteSet = CheerEmoteSet();
cheerEmoteSet.regex = QRegularExpression(
"^" + set.prefix.toLower() + "([1-9][0-9]*)$");
"^" + set.prefix + "([1-9][0-9]*)$",
QRegularExpression::CaseInsensitiveOption);
for (auto &tier : set.tiers) {
for (auto &tier : set.tiers)
{
CheerEmote cheerEmote;
cheerEmote.color = QColor(tier.color);
@ -733,7 +739,7 @@ void TwitchChannel::refreshCheerEmotes()
std::sort(cheerEmoteSet.cheerEmotes.begin(),
cheerEmoteSet.cheerEmotes.end(),
[](const auto &lhs, const auto &rhs) {
return lhs.minBits < rhs.minBits; //
return lhs.minBits > rhs.minBits;
});
emoteSets.emplace_back(cheerEmoteSet);
@ -741,10 +747,8 @@ void TwitchChannel::refreshCheerEmotes()
*this->cheerEmoteSets_.access() = std::move(emoteSets);
return Success;
});
request.execute();
*/
})
.execute();
}
boost::optional<EmotePtr> TwitchChannel::twitchBadge(
@ -765,9 +769,34 @@ boost::optional<EmotePtr> TwitchChannel::twitchBadge(
boost::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
{
if (auto badge = this->ffzCustomModBadge_.badge())
return badge;
return this->ffzCustomModBadge_.get();
}
boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
{
auto sets = this->cheerEmoteSets_.access();
for (const auto &set : *sets)
{
auto match = set.regex.match(string);
if (!match.hasMatch())
{
continue;
}
QString amount = match.captured(1);
bool ok = false;
int bitAmount = amount.toInt(&ok);
if (!ok)
{
log("Error parsing bit amount in cheerEmote");
}
for (const auto &emote : set.cheerEmotes)
{
if (bitAmount >= emote.minBits)
{
return emote;
}
}
}
return boost::none;
}

View file

@ -6,7 +6,7 @@
#include "common/ChannelChatters.hpp"
#include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp"
#include "providers/ffz/FfzModBadge.hpp"
#include "common/UsernameSet.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include <rapidjson/document.h>
@ -96,6 +96,9 @@ public:
boost::optional<EmotePtr> twitchBadge(const QString &set,
const QString &version) const;
// Cheers
boost::optional<CheerEmote> cheerEmote(const QString &string);
// Signals
pajlada::Signals::NoArgSignal roomIdChanged;
pajlada::Signals::NoArgSignal userStateChanged;
@ -145,13 +148,13 @@ protected:
FfzEmotes &globalFfz_;
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
Atomic<boost::optional<EmotePtr>> ffzCustomModBadge_;
private:
// Badges
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
FfzModBadge ffzCustomModBadge_;
bool mod_ = false;
bool vip_ = false;

View file

@ -143,9 +143,21 @@ 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 auto player = new QMediaPlayer;
static QUrl currentPlayerUrl;
if (this->historicalMessage_)
@ -164,6 +176,8 @@ void TwitchMessageBuilder::triggerHighlights()
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
if (this->highlightSound_ && resolveFocus)
{
if (auto player = getPlayer())
{
// update the media player url if necessary
QUrl highlightSoundUrl =
@ -181,6 +195,7 @@ void TwitchMessageBuilder::triggerHighlights()
player->play();
}
}
if (this->highlightAlert_)
{
@ -274,7 +289,7 @@ MessagePtr TwitchMessageBuilder::build()
if (iterator != this->tags.end())
{
this->hasBits_ = true;
// bits = iterator.value().toString();
this->bits = iterator.value().toString();
}
// twitch emotes
@ -340,7 +355,7 @@ void TwitchMessageBuilder::addWords(
auto i = int();
auto currentTwitchEmote = twitchEmotes.begin();
for (const auto &word : words)
for (auto word : words)
{
// check if it's a twitch emote twitch emote
while (currentTwitchEmote != twitchEmotes.end() &&
@ -361,10 +376,20 @@ void TwitchMessageBuilder::addWords(
MessageElementFlag::TwitchEmote);
i += word.length() + 1;
int len = std::get<2>(*currentTwitchEmote).string.length();
currentTwitchEmote++;
if (len < word.length())
{
word = word.mid(len);
this->message().elements.back()->setTrailingSpace(false);
}
else
{
continue;
}
}
// split words
for (auto &variant : getApp()->emotes->emojis.parse(word))
@ -952,7 +977,7 @@ void TwitchMessageBuilder::parseHighlights()
{
HighlightPhrase selfHighlight(
currentUsername, getSettings()->enableSelfHighlightTaskbar,
getSettings()->enableSelfHighlightSound, false);
getSettings()->enableSelfHighlightSound, false, false);
activeHighlights.emplace_back(std::move(selfHighlight));
}
@ -1171,7 +1196,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
{
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
{
this->emplace<BadgeElement>(
this->emplace<ModBadgeElement>(
customModBadge.get(),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip((*customModBadge)->tooltip.string);
@ -1285,56 +1310,29 @@ void TwitchMessageBuilder::appendChatterinoBadges()
}
}
Outcome TwitchMessageBuilder::tryParseCheermote(const QString & /*string*/)
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
{
// auto app = getApp();
//// Try to parse custom cheermotes
// const auto &channelResources = app->resources->channels[this->roomID_];
// if (channelResources.loaded) {
// for (const auto &cheermoteSet : channelResources.cheermoteSets) {
// auto match = cheermoteSet.regex.match(string);
// if (!match.hasMatch()) {
// continue;
// }
// QString amount = match.captured(1);
// bool ok = false;
// int numBits = amount.toInt(&ok);
// if (!ok) {
// Log("Error parsing bit amount in tryParseCheermote");
// return Failure;
// }
// auto savedIt = cheermoteSet.cheermotes.end();
// // Fetch cheermote that matches our numBits
// for (auto it = cheermoteSet.cheermotes.begin(); it !=
// cheermoteSet.cheermotes.end();
// ++it) {
// if (numBits >= it->minBits) {
// savedIt = it;
// } else {
// break;
// }
// }
// if (savedIt == cheermoteSet.cheermotes.end()) {
// Log("Error getting a cheermote from a cheermote set for the
// bit amount {}",
// numBits);
// return Failure;
// }
// const auto &cheermote = *savedIt;
// this->emplace<EmoteElement>(cheermote.animatedEmote,
// MessageElementFlag::BitsAnimated);
// this->emplace<TextElement>(amount, MessageElementFlag::Text,
// cheermote.color);
// return Success;
// }
//}
auto cheerOpt = this->twitchChannel->cheerEmote(string);
if (!cheerOpt)
{
return Failure;
}
auto &cheerEmote = *cheerOpt;
if (cheerEmote.staticEmote)
{
this->emplace<EmoteElement>(cheerEmote.staticEmote,
MessageElementFlag::BitsStatic);
}
if (cheerEmote.animatedEmote)
{
this->emplace<EmoteElement>(cheerEmote.animatedEmote,
MessageElementFlag::BitsAnimated);
}
if (cheerEmote.color != QColor())
{
this->emplace<TextElement>(this->bits, MessageElementFlag::BitsAmount,
cheerEmote.color);
}
return Success;
}
} // namespace chatterino

View file

@ -78,6 +78,7 @@ private:
QString roomID_;
bool hasBits_ = false;
QString bits;
bool historicalMessage_ = false;
QString userId_;

View file

@ -8,6 +8,7 @@
#include <QStandardPaths>
#include <cassert>
#include "common/Modes.hpp"
#include "util/CombinePath.hpp"
namespace chatterino {
@ -32,7 +33,7 @@ bool Paths::createFolder(const QString &folderPath)
bool Paths::isPortable()
{
return this->portable_.get();
return Modes::getInstance().isPortable;
}
QString Paths::cacheDirectory()

View file

@ -208,6 +208,7 @@ public:
BoolSetting loadTwitchMessageHistoryOnConnect = {
"/misc/twitch/loadMessageHistoryOnConnect", true};
IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 0};
BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0};
QStringSetting cachePath = {"/cache/path", ""};

View file

@ -1,6 +1,7 @@
#include "Updates.hpp"
#include "Settings.hpp"
#include "common/Modes.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "common/Version.hpp"
@ -201,6 +202,14 @@ void Updates::installUpdates()
void Updates::checkForUpdates()
{
// Disable updates if on nightly and windows.
#ifdef Q_OS_WIN
if (Modes::getInstance().isNightly)
{
return;
}
#endif
QString url =
"https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS "/" +
currentBranch();

View file

@ -82,8 +82,7 @@ void WindowManager::showAccountSelectPopup(QPoint point)
w->refresh();
QPoint buttonPos = point;
w->move(buttonPos.x(), buttonPos.y());
w->move(buttonPos.x() - 30, buttonPos.y());
w->show();
w->setFocus();
}

View file

@ -0,0 +1,64 @@
#pragma once
namespace chatterino {
std::vector<QString> getSampleCheerMessage()
{
// clang-format off
std::vector<QString> cheerMessageVector;
cheerMessageVector.push_back(R"(@badge-info=subscriber/4;badges=moderator/1,subscriber/3,sub-gifter/5;bits=2;color=#FF0000;display-name=69_faith_420;emotes=;flags=;id=c5fd49c7-ecbc-46dd-a790-c9f10fdaaa67;mod=1;room-id=111448817;subscriber=1;tmi-sent-ts=1567282184553;turbo=0;user-id=125608098;user-type=mod :69_faith_420!69_faith_420@69_faith_420.tmi.twitch.tv PRIVMSG #pajlada :cheer2 Stop what? I'm not doing anything.)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/4;badges=moderator/1,subscriber/3,sub-gifter/5;bits=2;color=#FF0000;display-name=69_faith_420;emotes=;flags=;id=397f4d2e-cac8-4689-922a-32709b9e8b4f;mod=1;room-id=111448817;subscriber=1;tmi-sent-ts=1567282159076;turbo=0;user-id=125608098;user-type=mod :69_faith_420!69_faith_420@69_faith_420.tmi.twitch.tv PRIVMSG #pajlada :cheer2 Who keeps getting their bits out now?)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/1;badges=subscriber/0,bits/1;bits=2;color=#FF0000;display-name=FlameGodFlann;emotes=;flags=;id=664ddc92-649d-4889-9641-208a6e62ef1e;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567282066199;turbo=0;user-id=56442185;user-type= :flamegodflann!flamegodflann@flamegodflann.tmi.twitch.tv PRIVMSG #pajlada :Cheer2 I'm saving my only can of Stella for your upcoming win, lets go!)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/3;badges=moderator/1,subscriber/3,bits/100;bits=10;color=#008000;display-name=k4izn;emotes=;flags=;id=3919af0b-93e0-412c-b238-d152f92ffea7;mod=1;room-id=111448817;subscriber=1;tmi-sent-ts=1567811485257;turbo=0;user-id=207114672;user-type=mod :k4izn!k4izn@k4izn.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Kleiner Cheer(s) !)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/12;badges=subscriber/12,bits/1000;bits=20;color=#00CCFF;display-name=YaBoiBurnsy;emotes=;flags=;id=5b53975d-b339-484f-a2a0-3ffbedde0df2;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567529634584;turbo=0;user-id=45258137;user-type= :yaboiburnsy!yaboiburnsy@yaboiburnsy.tmi.twitch.tv PRIVMSG #pajlada :ShowLove20)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/1;badges=moderator/1,subscriber/0,bits-leader/2;bits=1;color=;display-name=jdfellie;emotes=;flags=18-22:A.3/P.5;id=28c8f4b7-b1e3-4404-b0f8-5cfe46411ef9;mod=1;room-id=111448817;subscriber=1;tmi-sent-ts=1567668177856;turbo=0;user-id=137619637;user-type=mod :jdfellie!jdfellie@jdfellie.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 take a bit bitch)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/2;bits=30;color=#EC3B83;display-name=Sammay;emotes=;flags=;id=ccf058a6-c1f1-45de-a764-fc8f96f21449;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1566719874294;turbo=0;user-id=58283830;user-type= :sammay!sammay@sammay.tmi.twitch.tv PRIVMSG #pajlada :ShowLove30 @Emperor_Zhang)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/2;bits=6;color=#97E7FF;display-name=Emperor_Zhang;emotes=;flags=;id=53bab01b-9f6c-4123-a852-9916ab371cf9;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1566719803345;turbo=0;user-id=105292882;user-type= :emperor_zhang!emperor_zhang@emperor_zhang.tmi.twitch.tv PRIVMSG #pajlada :uni6)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=5;color=#97E7FF;display-name=Emperor_Zhang;emotes=;flags=;id=545caec6-8b5f-460a-8b4b-3e407e179689;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1566704926380;turbo=0;user-id=105292882;user-type= :emperor_zhang!emperor_zhang@emperor_zhang.tmi.twitch.tv PRIVMSG #pajlada :VoHiYo5)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/100;bits=50;color=;display-name=Schmiddi55;emotes=;flags=;id=777f1018-941d-48aa-bf4e-ed8053d556c8;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567708393343;turbo=0;user-id=101444120;user-type= :schmiddi55!schmiddi55@schmiddi55.tmi.twitch.tv PRIVMSG #pajlada :cheer50 sere ihr radlertrinker)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/3;badges=subscriber/3,sub-gifter/10;bits=100;color=#0000FF;display-name=MLPTheChad;emotes=;flags=87-91:P.5;id=ed7db31e-884b-4761-9c88-b1676caa8814;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567681752733;turbo=0;user-id=63179867;user-type= :mlpthechad!mlpthechad@mlpthechad.tmi.twitch.tv PRIVMSG #pajlada :Subway100 bonus10 Statistically speaking, 10 out of 10 constipated people don't give a shit.)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/3;badges=subscriber/3,sub-gifter/10;bits=100;color=#0000FF;display-name=MLPTheChad;emotes=;flags=;id=506b482a-515a-4914-a694-2c69d2add23a;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567681618814;turbo=0;user-id=63179867;user-type= :mlpthechad!mlpthechad@mlpthechad.tmi.twitch.tv PRIVMSG #pajlada :Subway100 bonus10 That's some SUB par gameplay, Dabier.)");
cheerMessageVector.push_back(R"(@badge-info=;badges=premium/1;bits=100;color=;display-name=AkiraKurusu__;emotes=;flags=;id=6e343f5d-0e0e-47f7-bf6d-d5d7bf18b95a;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567765732657;turbo=0;user-id=151679027;user-type= :akirakurusu__!akirakurusu__@akirakurusu__.tmi.twitch.tv PRIVMSG #pajlada :TriHard100)");
cheerMessageVector.push_back(R"(@badge-info=;badges=premium/1;bits=1;color=;display-name=AkiraKurusu__;emotes=;flags=;id=dfdf6c2f-abee-4a4b-99fe-0d0b221f07de;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567765295301;turbo=0;user-id=151679027;user-type= :akirakurusu__!akirakurusu__@akirakurusu__.tmi.twitch.tv PRIVMSG #pajlada :TriHard1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/100;bits=500;color=#0000FF;display-name=Stabbr;emotes=;flags=;id=e28b384e-fb6a-4da5-9a36-1b6153c6089d;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567648284623;turbo=0;user-id=183081176;user-type= :stabbr!stabbr@stabbr.tmi.twitch.tv PRIVMSG #pajlada :cheer500 Gotta be on top)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/1;badges=subscriber/0,bits-leader/1;bits=100;color=;display-name=dbf_sub;emotes=;flags=;id=7cf317b8-6e28-4615-a0ba-e0bbaa0d4b29;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567646349560;turbo=0;user-id=450101746;user-type= :dbf_sub!dbf_sub@dbf_sub.tmi.twitch.tv PRIVMSG #pajlada :EleGiggle100)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/1;badges=subscriber/0,bits/1;bits=1;color=;display-name=dbf_sub;emotes=;flags=;id=43b5fc97-e7cc-4ac1-8d7e-7504c435c3f1;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567643510222;turbo=0;user-id=450101746;user-type= :dbf_sub!dbf_sub@dbf_sub.tmi.twitch.tv PRIVMSG #pajlada :SeemsGood1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/2;bits=100;color=;display-name=RobertsonRobotics;emotes=;flags=;id=598dfa14-23e9-4e45-a2fe-7a0263828817;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567873463820;turbo=0;user-id=117177721;user-type= :robertsonrobotics!robertsonrobotics@robertsonrobotics.tmi.twitch.tv PRIVMSG #pajlada :firstCheer100 This is so cool! Cant wait for the competition!)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/100;bits=18;color=#1E90FF;display-name=Vipacman11;emotes=;flags=;id=07f59664-0c75-459e-b137-26c8d03e44be;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567873210379;turbo=0;user-id=89634839;user-type= :vipacman11!vipacman11@vipacman11.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=sub-gifter/5;bits=100;color=#FF7F50;display-name=darkside_sinner;emotes=;flags=;id=090102b3-369d-4ce4-ad1f-283849b10de0;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567822075293;turbo=0;user-id=104942909;user-type= :darkside_sinner!darkside_sinner@darkside_sinner.tmi.twitch.tv PRIVMSG #pajlada :Subway100 bonus10)");
cheerMessageVector.push_back(R"(@badge-info=;badges=sub-gifter/5;bits=200;color=#FF7F50;display-name=darkside_sinner;emotes=;flags=;id=2bdf7846-5ffa-4798-a397-997e7209a6d0;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567821695287;turbo=0;user-id=104942909;user-type= :darkside_sinner!darkside_sinner@darkside_sinner.tmi.twitch.tv PRIVMSG #pajlada :Subway200 bonus20)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=50;color=#0000FF;display-name=SincereBC;emotes=;flags=;id=b8c9236b-aeb9-4c72-a191-593e33c6c3f1;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567818308913;turbo=0;user-id=146097597;user-type= :sincerebc!sincerebc@sincerebc.tmi.twitch.tv PRIVMSG #pajlada :cheer50)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=1;color=#FF0000;display-name=AngryCh33s3puff;emotes=;flags=;id=6ab62185-ac1b-4ee5-bd93-165009917078;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567474810480;turbo=0;user-id=55399500;user-type= :angrych33s3puff!angrych33s3puff@angrych33s3puff.tmi.twitch.tv PRIVMSG #pajlada :cheer1 for the chair!)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/3;badges=moderator/1,subscriber/0,bits/1000;bits=1500;color=#5F9EA0;display-name=LaurenJW28;emotes=;flags=;id=2403678c-6109-43ac-b3b5-1f5230f91729;mod=1;room-id=111448817;subscriber=1;tmi-sent-ts=1567746107991;turbo=0;user-id=244354979;user-type=mod :laurenjw28!laurenjw28@laurenjw28.tmi.twitch.tv PRIVMSG #pajlada :Cheer1000 Cheer100 Cheer100 Cheer100 Cheer100 Cheer100)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=5;color=#5F9EA0;display-name=drkwings;emotes=;flags=;id=ad45dae5-b985-4526-9b9e-0bdba2d23289;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567742106689;turbo=0;user-id=440230526;user-type= :drkwings!drkwings@drkwings.tmi.twitch.tv PRIVMSG #pajlada :SeemsGood1 SeemsGood1 SeemsGood1 SeemsGood1 SeemsGood1)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/16;badges=subscriber/12,bits/1000;bits=1;color=;display-name=mustangbugatti;emotes=;flags=;id=ee987ee9-46a4-4c06-bf66-2cafff5d4cdd;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567883658780;turbo=0;user-id=115948494;user-type= :mustangbugatti!mustangbugatti@mustangbugatti.tmi.twitch.tv PRIVMSG #pajlada :(In clarkson accent) Some say...the only number in his contacts is himself..... And...that he is the international butt-dial champion... All we know is.... HES CALLED THE STIG Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/2;badges=subscriber/0,bits/1000;bits=1;color=;display-name=derpysaurus1;emotes=;flags=;id=c41c3d8b-c591-4db0-87e7-a78c5536de82;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567883655116;turbo=0;user-id=419221818;user-type= :derpysaurus1!derpysaurus1@derpysaurus1.tmi.twitch.tv PRIVMSG #pajlada :cheer1 OMG ur back yaaaaaaaaaaaaaaaaaaaaayyyyyyyyy)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/5;badges=subscriber/0,premium/1;bits=1;color=#8A2BE2;display-name=sirlordstallion;emotes=;flags=;id=61a87aeb-88b1-42f9-90f5-74429d8bf387;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567882978939;turbo=0;user-id=92145441;user-type= :sirlordstallion!sirlordstallion@sirlordstallion.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Alex is definetly not putting his eggs in Narreths basket)");
cheerMessageVector.push_back(R"(@badge-info=subscriber/1;badges=subscriber/0,bits/1;bits=1;color=;display-name=xplosivegingerx;emotes=;flags=;id=f8aac1e0-050a-44bf-abcc-c0cf12cbedfc;mod=0;room-id=111448817;subscriber=1;tmi-sent-ts=1567882249072;turbo=0;user-id=151265906;user-type= :xplosivegingerx!xplosivegingerx@xplosivegingerx.tmi.twitch.tv PRIVMSG #pajlada :Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/100;bits=500;color=;display-name=AlexJohanning;emotes=;flags=;id=4e4229a3-e7f2-4082-8c55-47d42db3b09c;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567881969862;turbo=0;user-id=190390930;user-type= :alexjohanning!alexjohanning@alexjohanning.tmi.twitch.tv PRIVMSG #pajlada :cheer500)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=245;color=;display-name=undonebunion6;emotes=;flags=;id=331ec583-0a80-4299-9206-0efd9e33d934;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567881553759;turbo=0;user-id=452974274;user-type= :undonebunion6!undonebunion6@undonebunion6.tmi.twitch.tv PRIVMSG #pajlada :cheer245 can I join?)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/100;bits=100;color=;display-name=therealruffnix;emotes=;flags=61-67:S.6;id=25f567ad-ac95-45ab-b12e-4d647f6a2345;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567524218162;turbo=0;user-id=55059620;user-type= :therealruffnix!therealruffnix@therealruffnix.tmi.twitch.tv PRIVMSG #pajlada :cheer100 This is the kind of ASMR I'm missing on YouTube and PornHub)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=1;color=;display-name=BeamMeUpSnotty;emotes=;flags=;id=8022f41f-dcb8-42f2-b46a-04d4a99180bd;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567270037926;turbo=0;user-id=261679182;user-type= :beammeupsnotty!beammeupsnotty@beammeupsnotty.tmi.twitch.tv PRIVMSG #pajlada :SeemsGood1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=10;color=#00FF7F;display-name=EXDE_HUN;emotes=;flags=;id=60d8835b-23fa-418c-96ca-5874e5d5e8ba;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1566654664248;turbo=0;user-id=129793695;user-type= :exde_hun!exde_hun@exde_hun.tmi.twitch.tv PRIVMSG #pajlada :PogChamp10)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/3;bits=5;color=;display-name=slyckity;emotes=;flags=;id=fd6c5507-3a4e-4d24-8f6e-fadf07f520d3;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824273752;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/3;bits=5;color=;display-name=slyckity;emotes=;flags=;id=7003f119-b9a6-4319-a1e8-8e99f96ab01a;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824186437;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/3;bits=10;color=;display-name=slyckity;emotes=;flags=;id=3f7de686-77f6-46d2-919e-404312c6676f;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824128736;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/3;bits=10;color=;display-name=slyckity;emotes=;flags=;id=9e830ed3-8735-4ccb-9a8b-80466598ca19;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567824118921;turbo=0;user-id=143114011;user-type= :slyckity!slyckity@slyckity.tmi.twitch.tv PRIVMSG #pajlada :Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1 Cheer1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=377;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=262f4d54-9b21-4f13-aac3-6d3b1051282f;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440897074;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :NotLikeThis377)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=144;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=3556e0ad-b5f8-4190-9c4c-e39c1940d191;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440861545;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :bday144)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=89;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=96e380a5-786d-44b8-819a-529b6adb06ac;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440848361;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :SwiftRage89)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=34;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=76239011-65fa-4f6a-a6d6-dc5d5dcbd674;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440816630;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :MrDestructoid34)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=21;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=4c05c97c-7b6c-4ae9-bc91-04e98240c1d5;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440806389;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :TriHard21)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=8;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=3b2ecce7-842e-429e-b6c8-9456c4646362;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440774009;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :EleGiggle8)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=5;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=3b8736d1-832d-4152-832a-50c526714fd1;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440762580;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :uni5)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=3;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=c13a1540-2a03-4c7d-af50-cb20ed88cefd;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440750103;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :Party3)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/1;bits=2;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=5d889eeb-b6b9-4a4e-91ff-0aecdf297edd;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440738337;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :ShowLove2)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=1;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=da47f91a-40d3-4209-ba1c-0219d8b8ecaf;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440720363;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :Scoops1)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits/1;bits=10;color=#8A2BE2;display-name=EkimSky;emotes=;flags=;id=8adea5b4-7430-44ea-a666-5ebaceb69441;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567833047623;turbo=0;user-id=42132818;user-type= :ekimsky!ekimsky@ekimsky.tmi.twitch.tv PRIVMSG #pajlada :Hi Cheer10)");
cheerMessageVector.push_back(R"(@badge-info=;badges=bits-leader/2;bits=500;color=;display-name=godkiller76;emotes=;flags=;id=80e86bcc-d048-44f3-8073-9a1014568e0c;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567753685704;turbo=0;user-id=258838478;user-type= :godkiller76!godkiller76@godkiller76.tmi.twitch.tv PRIVMSG #pajlada :Party100 Party100 Party100 Party100 Party100)");
return cheerMessageVector;
// clang-format on
};
} // namespace chatterino

View file

@ -11,9 +11,8 @@
namespace chatterino {
AccountSwitchPopup::AccountSwitchPopup(QWidget *parent)
: QWidget(parent)
: BaseWindow({BaseWindow::TopMost, BaseWindow::Frameless}, parent)
{
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
#ifdef Q_OS_LINUX
this->setWindowFlag(Qt::Popup);
#endif
@ -25,8 +24,6 @@ AccountSwitchPopup::AccountSwitchPopup(QWidget *parent)
this->ui_.accountSwitchWidget->setFocusPolicy(Qt::NoFocus);
vbox->addWidget(this->ui_.accountSwitchWidget);
// vbox->setSizeConstraint(QLayout::SetMinimumSize);
auto hbox = new QHBoxLayout();
auto manageAccountsButton = new QPushButton(this);
manageAccountsButton->setText("Manage Accounts");
@ -38,9 +35,9 @@ AccountSwitchPopup::AccountSwitchPopup(QWidget *parent)
SettingsDialog::showDialog(SettingsDialogPreference::Accounts); //
});
this->setLayout(vbox);
this->getLayoutContainer()->setLayout(vbox);
// this->setStyleSheet("background: #333");
this->setScaleIndependantSize(200, 200);
}
void AccountSwitchPopup::refresh()

View file

@ -1,12 +1,13 @@
#pragma once
#include "widgets/AccountSwitchWidget.hpp"
#include "widgets/BaseWindow.hpp"
#include <QWidget>
namespace chatterino {
class AccountSwitchPopup : public QWidget
class AccountSwitchPopup : public BaseWindow
{
Q_OBJECT

View file

@ -2,6 +2,7 @@
#include "Application.hpp"
#include "common/Credentials.hpp"
#include "common/Modes.hpp"
#include "common/Version.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
@ -23,6 +24,10 @@
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
#ifdef QT_DEBUG
# include "util/SampleCheerMessages.hpp"
#endif
#include <QApplication>
#include <QDesktopServices>
#include <QHeaderView>
@ -36,7 +41,7 @@
namespace chatterino {
Window::Window(WindowType type)
: BaseWindow(nullptr, BaseWindow::EnableCustomFrame)
: BaseWindow(BaseWindow::EnableCustomFrame)
, type_(type)
, notebook_(new SplitNotebook(this))
{
@ -112,18 +117,10 @@ bool Window::event(QEvent *event)
void Window::showEvent(QShowEvent *event)
{
// Startup notification
if (getSettings()->startUpNotification.getValue() < 1)
/*if (getSettings()->startUpNotification.getValue() < 1)
{
getSettings()->startUpNotification = 1;
// auto box = new QMessageBox(
// QMessageBox::Information, "Chatterino 2 Beta",
// "Please note that this software is not stable yet. Things are "
// "rough "
// "around the edges and everything is subject to change.");
// box->setAttribute(Qt::WA_DeleteOnClose);
// box->show();
}
}*/
// Show changelog
if (getSettings()->currentVersion.getValue() != "" &&
@ -205,16 +202,9 @@ void Window::addDebugStuff()
{
#ifdef QT_DEBUG
std::vector<QString> cheerMessages, subMessages, miscMessages;
cheerMessages = getSampleCheerMessage();
// clang-format off
cheerMessages.emplace_back(R"(@badges=subscriber/12,premium/1;bits=2000;color=#B22222;display-name=arzenhuz;emotes=185989:33-37;id=1ae336ac-8e1a-4d6b-8b00-9fcee26e8337;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1515783470139;turbo=0;user-id=111553331;user-type= :arzenhuz!arzenhuz@arzenhuz.tmi.twitch.tv PRIVMSG #pajlada :pajacheer2000 Buy pizza for both pajaH)");
cheerMessages.emplace_back(R"(@badges=subscriber/12,premium/1;bits=37;color=#3FBF72;display-name=VADIKUS007;emotes=;id=eedd95fd-2a17-4da1-879c-a1e76ffce582;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1515783184352;turbo=0;user-id=72256775;user-type= :vadikus007!vadikus007@vadikus007.tmi.twitch.tv PRIVMSG #pajlada :cheer37)");
cheerMessages.emplace_back(R"(@badges=moderator/1,subscriber/24,bits/100;bits=1;color=#DCD3E6;display-name=swiftapples;emotes=80803:7-13;id=1c4647f6-f1a8-4acc-a9b2-b5d23d91258d;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1515538318854;turbo=0;user-id=80526177;user-type=mod :swiftapples!swiftapples@swiftapples.tmi.twitch.tv PRIVMSG #pajlada :cheer1 pajaHey)");
cheerMessages.emplace_back(R"(@badges=subscriber/12,turbo/1;bits=1;color=#0A2927;display-name=Binkelderk;emotes=;id=a1d9bdc6-6f6a-4c03-8554-d5b34721a878;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1515538899479;turbo=1;user-id=89081828;user-type= :binkelderk!binkelderk@binkelderk.tmi.twitch.tv PRIVMSG #pajlada :pajacheer1)");
cheerMessages.emplace_back(R"(@badges=moderator/1,subscriber/24,bits/100;bits=1;color=#DCD3E6;display-name=swiftapples;emotes=80803:6-12;id=e9e21793-0b58-4ac6-8a1e-c19e165dbc9f;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1515539073209;turbo=0;user-id=80526177;user-type=mod :swiftapples!swiftapples@swiftapples.tmi.twitch.tv PRIVMSG #pajlada :bday1 pajaHey)");
cheerMessages.emplace_back(R"(@badges=partner/1;bits=1;color=#CC44FF;display-name=pajlada;emotes=;id=ca89214e-4fb5-48ec-853e-d2e6b41355ea;mod=0;room-id=39705480;subscriber=0;tmi-sent-ts=1515546977622;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #leesherwhy :test123 owocheer1 456test)");
cheerMessages.emplace_back(R"(@badges=subscriber/12,premium/1;bits=1;color=#3FBF72;display-name=VADIKUS007;emotes=;id=c4c5061b-f5c6-464b-8bff-7f1ac816caa7;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1515782817171;turbo=0;user-id=72256775;user-type= :vadikus007!vadikus007@vadikus007.tmi.twitch.tv PRIVMSG #pajlada :trihard1)");
cheerMessages.emplace_back(R"(@badges=;bits=1;color=#FF0000;display-name=?????;emotes=;id=979b6b4f-be9a-42fb-a54c-88fcb0aca18d;mod=0;room-id=11148817;subscriber=0;tmi-sent-ts=1515782819084;turbo=0;user-id=70656218;user-type= :stels_tv!stels_tv@stels_tv.tmi.twitch.tv PRIVMSG #pajlada :trihard1)");
cheerMessages.emplace_back(R"(@badges=subscriber/3,premium/1;bits=1;color=#FF0000;display-name=kalvarenga;emotes=;id=4744d6f0-de1d-475d-a3ff-38647113265a;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1515782860740;turbo=0;user-id=108393131;user-type= :kalvarenga!kalvarenga@kalvarenga.tmi.twitch.tv PRIVMSG #pajlada :trihard1)");
subMessages.emplace_back(R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=1337;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)");
subMessages.emplace_back(R"(@badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=89614178;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\sof\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\sgifted\sa\sTier\s1\ssub\sto\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=13405587;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada)");
@ -252,11 +242,19 @@ void Window::addDebugStuff()
app->twitch.server->addFakeMessage(msg);
});
createWindowShortcut(this, "F7", [=] {
const auto &messages = cheerMessages;
static int index = 0;
const auto &msg = messages[index++ % messages.size()];
getApp()->twitch.server->addFakeMessage(msg);
});
createWindowShortcut(this, "F9", [=] {
auto *dialog = new WelcomeDialog();
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
});
#endif
}
@ -373,16 +371,25 @@ void Window::onAccountSelected()
{
auto user = getApp()->accounts->twitch.getCurrent();
//#ifdef CHATTERINO_NIGHTLY_VERSION_STRING
// auto windowTitleEnd =
// QString("Chatterino Nightly " CHATTERINO_VERSION
// " (" UGLYMACROHACK(CHATTERINO_NIGHTLY_VERSION_STRING) ")");
//#else
auto windowTitleEnd = QString("Chatterino " CHATTERINO_VERSION);
//#endif
// update title
QString title = "Chatterino ";
if (Modes::getInstance().isNightly)
{
title += "Nightly ";
}
title += CHATTERINO_VERSION;
this->setWindowTitle(windowTitleEnd);
if (Modes::getInstance().isNightly)
{
#ifdef CHATTERINO_NIGHTLY_VERSION_STRING
title +=
QString(" (" UGLYMACROHACK(CHATTERINO_NIGHTLY_VERSION_STRING) ")");
#endif
}
this->setWindowTitle(title);
// update user
if (user->isAnon())
{
if (this->userLabel_)
@ -397,6 +404,6 @@ void Window::onAccountSelected()
this->userLabel_->getLabel().setText(user->getUserName());
}
}
}
} // namespace chatterino
} // namespace chatterino

View file

@ -101,7 +101,7 @@ namespace {
} // namespace
EmotePopup::EmotePopup(QWidget *parent)
: BaseWindow(parent, BaseWindow::EnableCustomFrame)
: BaseWindow(BaseWindow::EnableCustomFrame, parent)
{
auto layout = new QVBoxLayout(this);
this->getLayoutContainer()->setLayout(layout);

View file

@ -4,6 +4,7 @@
#include "common/Channel.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
@ -20,34 +21,30 @@ namespace chatterino {
LogsPopup::LogsPopup()
: channel_(Channel::getEmpty())
{
this->initLayout();
this->resize(400, 600);
}
void LogsPopup::initLayout()
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(0);
this->channelView_ = new ChannelView(this);
layout->addWidget(this->channelView_);
this->setLayout(layout);
}
void LogsPopup::setChannelName(QString channelName)
{
this->channelName_ = channelName;
}
void LogsPopup::setChannel(std::shared_ptr<Channel> channel)
void LogsPopup::setChannel(const ChannelPtr &channel)
{
this->channel_ = channel;
this->updateWindowTitle();
}
void LogsPopup::setTargetUserName(QString userName)
void LogsPopup::setChannelName(const QString &channelName)
{
this->channelName_ = channelName;
this->updateWindowTitle();
}
void LogsPopup::setTargetUserName(const QString &userName)
{
this->userName_ = userName;
this->updateWindowTitle();
}
void LogsPopup::updateWindowTitle()
{
this->setWindowTitle(this->userName_ + "'s logs in #" + this->channelName_);
}
void LogsPopup::getLogs()
@ -60,8 +57,6 @@ void LogsPopup::getLogs()
this->channelName_ = twitchChannel->getName();
this->getLogviewerLogs(twitchChannel->roomId());
this->setWindowTitle(this->userName_ + "'s logs in #" +
this->channelName_);
return;
}
}
@ -83,7 +78,7 @@ void LogsPopup::setMessages(std::vector<MessagePtr> &messages)
ChannelPtr logsChannel(new Channel("logs", Channel::Type::Misc));
logsChannel->addMessagesAtStart(messages);
this->channelView_->setChannel(logsChannel);
SearchPopup::setChannel(logsChannel);
}
void LogsPopup::getLogviewerLogs(const QString &roomID)
@ -121,6 +116,8 @@ void LogsPopup::getLogviewerLogs(const QString &roomID)
static_cast<Communi::IrcPrivateMessage *>(ircMessage);
TwitchMessageBuilder builder(this->channel_.get(), privMsg,
args);
builder.message().searchText = message;
messages.push_back(builder.build());
}
@ -165,6 +162,7 @@ void LogsPopup::getOverrustleLogs()
for (auto i : dataMessages)
{
QJsonObject singleMessage = i.toObject();
auto text = singleMessage.value("text").toString();
QTime timeStamp =
QDateTime::fromSecsSinceEpoch(
singleMessage.value("timestamp").toInt())
@ -175,9 +173,10 @@ void LogsPopup::getOverrustleLogs()
builder.emplace<TextElement>(this->userName_,
MessageElementFlag::Username,
MessageColor::System);
builder.emplace<TextElement>(
singleMessage.value("text").toString(),
MessageElementFlag::Text, MessageColor::Text);
builder.emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::Text);
builder.message().messageText = text;
builder.message().displayName = this->userName_;
messages.push_back(builder.release());
}
}
@ -191,4 +190,5 @@ void LogsPopup::getOverrustleLogs()
})
.execute();
}
} // namespace chatterino

View file

@ -1,37 +1,29 @@
#pragma once
#include "widgets/BaseWindow.hpp"
#include "widgets/helper/SearchPopup.hpp"
namespace chatterino {
class Channel;
class ChannelView;
class Channel;
using ChannelPtr = std::shared_ptr<Channel>;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
class LogsPopup : public BaseWindow
class LogsPopup : public SearchPopup
{
public:
LogsPopup();
void setChannelName(QString channelName);
void setChannel(std::shared_ptr<Channel> channel);
void setTargetUserName(QString userName);
void setChannel(const ChannelPtr &channel) override;
void setChannelName(const QString &channelName);
void setTargetUserName(const QString &userName);
void getLogs();
protected:
void updateWindowTitle() override;
private:
ChannelView *channelView_ = nullptr;
ChannelPtr channel_;
QString userName_;
QString channelName_;
void initLayout();
void setMessages(std::vector<MessagePtr> &messages);
void getOverrustleLogs();
void getLogviewerLogs(const QString &roomID);

View file

@ -11,7 +11,7 @@
namespace chatterino {
NotificationPopup::NotificationPopup()
: BaseWindow((QWidget *)nullptr, BaseWindow::Frameless)
: BaseWindow(BaseWindow::Frameless)
, channel_(std::make_shared<Channel>("notifications", Channel::Type::None))
{

View file

@ -24,7 +24,7 @@ namespace chatterino {
SettingsDialog *SettingsDialog::handle = nullptr;
SettingsDialog::SettingsDialog()
: BaseWindow(nullptr, BaseWindow::DisableCustomScaling)
: BaseWindow(BaseWindow::DisableCustomScaling)
{
this->setWindowTitle("Chatterino Settings");
@ -41,7 +41,7 @@ SettingsDialog::SettingsDialog()
void SettingsDialog::initUi()
{
auto outerBox = LayoutCreator<SettingsDialog>(this)
auto outerBox = LayoutCreator<QWidget>(this->getLayoutContainer())
.setLayoutType<QVBoxLayout>()
.withoutSpacing();

View file

@ -11,9 +11,8 @@
namespace chatterino {
UpdateDialog::UpdateDialog()
: BaseWindow(nullptr,
BaseWindow::Flags(BaseWindow::Frameless | BaseWindow::TopMost |
BaseWindow::EnableCustomFrame))
: BaseWindow({BaseWindow::Frameless, BaseWindow::TopMost,
BaseWindow::EnableCustomFrame})
{
auto layout =
LayoutCreator<UpdateDialog>(this).setLayoutType<QVBoxLayout>();

View file

@ -43,8 +43,7 @@ namespace {
} // namespace
UserInfoPopup::UserInfoPopup()
: BaseWindow(nullptr, BaseWindow::Flags(BaseWindow::Frameless |
BaseWindow::FramelessDraggable))
: BaseWindow({BaseWindow::Frameless, BaseWindow::FramelessDraggable})
, hack_(new bool)
{
this->setStayInScreenRect(true);
@ -55,8 +54,8 @@ UserInfoPopup::UserInfoPopup()
auto app = getApp();
auto layout =
LayoutCreator<UserInfoPopup>(this).setLayoutType<QVBoxLayout>();
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
.setLayoutType<QVBoxLayout>();
// first line
auto head = layout.emplace<QHBoxLayout>().withoutMargin();
@ -223,26 +222,32 @@ UserInfoPopup::UserInfoPopup()
});
}
// this->setStyleSheet("font-size: 11pt;");
this->installEvents();
this->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Policy::Ignored);
}
void UserInfoPopup::themeChangedEvent()
{
BaseWindow::themeChangedEvent();
this->setStyleSheet(
"background: #333; font-size: " +
QString::number(getFonts()
->getFont(FontStyle::UiMediumBold, this->scale())
.pixelSize()) +
"px;");
for (auto &&child : this->findChildren<QCheckBox *>())
{
child->setFont(getFonts()->getFont(FontStyle::UiMedium, this->scale()));
}
}
void UserInfoPopup::scaleChangedEvent(float /*scale*/)
{
themeChangedEvent();
QTimer::singleShot(20, this, [this] {
auto geo = this->geometry();
geo.setWidth(10);
geo.setHeight(10);
this->setGeometry(geo);
});
}
void UserInfoPopup::installEvents()

View file

@ -3,7 +3,7 @@
namespace chatterino {
WelcomeDialog::WelcomeDialog()
: BaseWindow(nullptr, BaseWindow::EnableCustomFrame)
: BaseWindow(BaseWindow::EnableCustomFrame)
{
this->setWindowTitle("Chatterino quick setup");
}

View file

@ -133,6 +133,8 @@ ChannelView::ChannelView(BaseWidget *parent)
this->clickTimer_ = new QTimer(this);
this->clickTimer_->setSingleShot(true);
this->clickTimer_->setInterval(500);
this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
}
void ChannelView::initializeLayout()
@ -1526,14 +1528,8 @@ void ChannelView::addContextMenuItems(
QString url = hoveredElement->getLink().value;
// open link
bool incognitoByDefault = supportsIncognitoLinks() &&
layout->getMessage()->loginName == "hemirt";
menu->addAction("Open link", [url, incognitoByDefault] {
if (incognitoByDefault)
openLinkIncognito(url);
else
QDesktopServices::openUrl(QUrl(url));
});
menu->addAction("Open link",
[url] { QDesktopServices::openUrl(QUrl(url)); });
// open link default
if (supportsIncognitoLinks())
{
@ -1699,6 +1695,9 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
case Link::Url:
{
if (getSettings()->openLinksIncognito && supportsIncognitoLinks())
openLinkIncognito(link.value);
else
QDesktopServices::openUrl(QUrl(link.value));
}
break;

View file

@ -7,23 +7,70 @@
#include "common/Channel.hpp"
#include "messages/Message.hpp"
#include "messages/search/AuthorPredicate.hpp"
#include "messages/search/LinkPredicate.hpp"
#include "messages/search/SubstringPredicate.hpp"
#include "widgets/helper/ChannelView.hpp"
namespace chatterino {
ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
const LimitedQueueSnapshot<MessagePtr> &snapshot)
{
ChannelPtr channel(new Channel(channelName, Channel::Type::None));
// Parse predicates from tags in "text"
auto predicates = parsePredicates(text);
// Check for every message whether it fulfills all predicates that have
// been registered
for (size_t i = 0; i < snapshot.size(); ++i)
{
MessagePtr message = snapshot[i];
bool accept = true;
for (const auto &pred : predicates)
{
// Discard the message as soon as one predicate fails
if (!pred->appliesTo(*message))
{
accept = false;
break;
}
}
// If all predicates match, add the message to the channel
if (accept)
channel->addMessage(message);
}
return channel;
}
SearchPopup::SearchPopup()
{
this->initLayout();
this->resize(400, 600);
}
void SearchPopup::setChannel(ChannelPtr channel)
void SearchPopup::setChannel(const ChannelPtr &channel)
{
this->channelName_ = channel->getName();
this->snapshot_ = channel->getMessageSnapshot();
this->performSearch();
this->search();
this->setWindowTitle("Searching in " + channel->getName() + "s history");
this->updateWindowTitle();
}
void SearchPopup::updateWindowTitle()
{
this->setWindowTitle("Searching in " + this->channelName_ + "s history");
}
void SearchPopup::search()
{
this->channelView_->setChannel(filter(this->searchInput_->text(),
this->channelName_, this->snapshot_));
}
void SearchPopup::keyPressEvent(QKeyEvent *e)
@ -43,18 +90,20 @@ void SearchPopup::initLayout()
{
QVBoxLayout *layout1 = new QVBoxLayout(this);
layout1->setMargin(0);
layout1->setSpacing(0);
// HBOX
{
QHBoxLayout *layout2 = new QHBoxLayout(this);
layout2->setMargin(6);
layout2->setMargin(8);
layout2->setSpacing(8);
// SEARCH INPUT
{
this->searchInput_ = new QLineEdit(this);
layout2->addWidget(this->searchInput_);
QObject::connect(this->searchInput_, &QLineEdit::returnPressed,
[this] { this->performSearch(); });
[this] { this->search(); });
}
// SEARCH BUTTON
@ -63,7 +112,7 @@ void SearchPopup::initLayout()
searchButton->setText("Search");
layout2->addWidget(searchButton);
QObject::connect(searchButton, &QPushButton::clicked,
[this] { this->performSearch(); });
[this] { this->search(); });
}
layout1->addLayout(layout2);
@ -80,25 +129,55 @@ void SearchPopup::initLayout()
}
}
void SearchPopup::performSearch()
std::vector<std::unique_ptr<MessagePredicate>> SearchPopup::parsePredicates(
const QString &input)
{
QString text = searchInput_->text();
static QRegularExpression predicateRegex(R"(^(\w+):([\w,]+)$)");
ChannelPtr channel(new Channel(this->channelName_, Channel::Type::None));
auto predicates = std::vector<std::unique_ptr<MessagePredicate>>();
auto words = input.split(' ', QString::SkipEmptyParts);
auto authors = QStringList();
for (size_t i = 0; i < this->snapshot_.size(); i++)
for (auto it = words.begin(); it != words.end();)
{
MessagePtr message = this->snapshot_[i];
if (text.isEmpty() ||
message->searchText.indexOf(this->searchInput_->text(), 0,
Qt::CaseInsensitive) != -1)
if (auto match = predicateRegex.match(*it); match.hasMatch())
{
channel->addMessage(message);
QString name = match.captured(1);
QString value = match.captured(2);
bool remove = true;
// match predicates
if (name == "from")
{
authors.append(value);
}
else if (name == "has" && value == "link")
{
predicates.push_back(std::make_unique<LinkPredicate>());
}
else
{
remove = false;
}
// remove or advance
it = remove ? words.erase(it) : ++it;
}
else
{
++it;
}
}
this->channelView_->setChannel(channel);
if (!authors.empty())
predicates.push_back(std::make_unique<AuthorPredicate>(authors));
if (!words.empty())
predicates.push_back(
std::make_unique<SubstringPredicate>(words.join(" ")));
return predicates;
}
} // namespace chatterino

View file

@ -1,6 +1,8 @@
#pragma once
#include "ForwardDecl.hpp"
#include "messages/LimitedQueueSnapshot.hpp"
#include "messages/search/MessagePredicate.hpp"
#include "widgets/BaseWindow.hpp"
#include <memory>
@ -9,30 +11,50 @@ class QLineEdit;
namespace chatterino {
class Channel;
class ChannelView;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
class SearchPopup : public BaseWindow
{
public:
SearchPopup();
void setChannel(std::shared_ptr<Channel> channel);
virtual void setChannel(const ChannelPtr &channel);
protected:
void keyPressEvent(QKeyEvent *e) override;
virtual void updateWindowTitle();
private:
void initLayout();
void performSearch();
void search();
/**
* @brief Only retains those message from a list of messages that satisfy a
* search query.
*
* @param text the search query -- will be parsed for MessagePredicates
* @param channelName name of the channel to be returned
* @param snapshot list of messages to filter
*
* @return a ChannelPtr with "channelName" and the filtered messages from
* "snapshot"
*/
static ChannelPtr filter(const QString &text, const QString &channelName,
const LimitedQueueSnapshot<MessagePtr> &snapshot);
/**
* @brief Checks the input for tags and registers their corresponding
* predicates.
*
* @param input the string to check for tags
* @return a vector of MessagePredicates requested in the input
*/
static std::vector<std::unique_ptr<MessagePredicate>> parsePredicates(
const QString &input);
LimitedQueueSnapshot<MessagePtr> snapshot_;
QLineEdit *searchInput_;
ChannelView *channelView_;
QString channelName_;
QLineEdit *searchInput_{};
ChannelView *channelView_{};
QString channelName_{};
};
} // namespace chatterino

View file

@ -11,6 +11,7 @@
#include "singletons/WindowManager.hpp"
#include "util/FuzzyConvert.hpp"
#include "util/Helpers.hpp"
#include "util/IncognitoBrowser.hpp"
#include "widgets/BaseWindow.hpp"
#include "widgets/helper/Line.hpp"
@ -377,6 +378,12 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addTitle("Miscellaneous");
if (supportsIncognitoLinks())
{
layout.addCheckbox("Open links in incognito/private mode",
s.openLinksIncognito);
}
#ifdef Q_OS_LINUX
if (!getPaths()->isPortable())
{

View file

@ -57,7 +57,7 @@ HighlightingPage::HighlightingPage()
view->addRegexHelpLink();
view->setTitles({"Pattern", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex"});
"Enable\nregex", "Case-\nsensitive"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -71,8 +71,8 @@ HighlightingPage::HighlightingPage()
});
view->addButtonPressed.connect([] {
getApp()->highlights->phrases.appendItem(
HighlightPhrase{"my phrase", true, false, false});
getApp()->highlights->phrases.appendItem(HighlightPhrase{
"my phrase", true, false, false, false});
});
}
@ -87,6 +87,10 @@ HighlightingPage::HighlightingPage()
.getElement();
view->addRegexHelpLink();
view->getTableView()->horizontalHeader()->hideSection(4);
// Case-sensitivity doesn't make sense for user names so it is
// set to "false" by default & no checkbox is shown
view->setTitles({"Username", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -103,7 +107,7 @@ HighlightingPage::HighlightingPage()
view->addButtonPressed.connect([] {
getApp()->highlights->highlightedUsers.appendItem(
HighlightPhrase{"highlighted user", true, false,
HighlightPhrase{"highlighted user", true, false, false,
false});
});
}

View file

@ -52,7 +52,7 @@ namespace {
const QString &title, const QString &description)
{
auto window =
new BaseWindow(parent, BaseWindow::Flags::EnableCustomFrame);
new BaseWindow(BaseWindow::Flags::EnableCustomFrame, parent);
window->setWindowTitle("Chatterino - " + title);
window->setAttribute(Qt::WA_DeleteOnClose);
auto layout = new QVBoxLayout();
@ -89,6 +89,7 @@ Split::Split(QWidget *parent)
{
this->setMouseTracking(true);
this->view_->setPausable(true);
this->view_->setFocusPolicy(Qt::FocusPolicy::NoFocus);
this->vbox_->setSpacing(0);
this->vbox_->setMargin(1);