Merge branch 'master' into apa-notification-on-live

This commit is contained in:
pajlada 2018-09-16 17:43:53 +02:00 committed by GitHub
commit e2a7765964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 628 additions and 227 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
__pycache__/
# C++ objects and libs # C++ objects and libs
*.slo *.slo

View file

@ -240,6 +240,7 @@ SOURCES += \
src/messages/Emote.cpp \ src/messages/Emote.cpp \
src/messages/ImageSet.cpp \ src/messages/ImageSet.cpp \
src/providers/bttv/BttvEmotes.cpp \ src/providers/bttv/BttvEmotes.cpp \
src/providers/LinkResolver.cpp \
src/providers/ffz/FfzEmotes.cpp \ src/providers/ffz/FfzEmotes.cpp \
src/autogenerated/ResourcesAutogen.cpp \ src/autogenerated/ResourcesAutogen.cpp \
src/singletons/Badges.cpp \ src/singletons/Badges.cpp \
@ -439,6 +440,7 @@ HEADERS += \
src/messages/ImageSet.hpp \ src/messages/ImageSet.hpp \
src/common/Outcome.hpp \ src/common/Outcome.hpp \
src/providers/bttv/BttvEmotes.hpp \ src/providers/bttv/BttvEmotes.hpp \
src/providers/LinkResolver.hpp \
src/providers/ffz/FfzEmotes.hpp \ src/providers/ffz/FfzEmotes.hpp \
src/autogenerated/ResourcesAutogen.hpp \ src/autogenerated/ResourcesAutogen.hpp \
src/singletons/Badges.hpp \ src/singletons/Badges.hpp \

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

View file

@ -5,10 +5,18 @@ from _generate_resources import *
ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc', ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc',
'generate_resources.py', '_generate_resources.py'] 'generate_resources.py', '_generate_resources.py']
# to ignore all files in a/b, add a/b to ignored_directories.
# this will ignore a/b/c/d.txt and a/b/xd.txt
ignored_directories = ['__pycache__'] ignored_directories = ['__pycache__']
def isNotIgnored(file): def isNotIgnored(file):
return str(file) not in ignored_files # check if file exists in an ignored direcory
for ignored_directory in ignored_directories:
if file.parent.as_posix().startswith(ignored_directory):
return False
return file.as_posix() not in ignored_files
all_files = list(filter(isNotIgnored, \ all_files = list(filter(isNotIgnored, \
filter(Path.is_file, Path('.').glob('**/*')))) filter(Path.is_file, Path('.').glob('**/*'))))
@ -18,15 +26,15 @@ image_files = list(filter(isNotIgnored, \
with open('./resources_autogenerated.qrc', 'w') as out: with open('./resources_autogenerated.qrc', 'w') as out:
out.write(resources_header) out.write(resources_header)
for file in all_files: for file in all_files:
out.write(f" <file>{str(file)}</file>\n") out.write(f" <file>{file.as_posix()}</file>\n")
out.write(resources_footer) out.write(resources_footer)
with open('../src/autogenerated/ResourcesAutogen.cpp', 'w') as out: with open('../src/autogenerated/ResourcesAutogen.cpp', 'w') as out:
out.write(source_header) out.write(source_header)
for file in sorted(image_files): for file in sorted(image_files):
var_name = str(file.with_suffix("")).replace("/",".") var_name = file.with_suffix("").as_posix().replace("/",".")
out.write(f' this->{var_name}') out.write(f' this->{var_name}')
out.write(f' = QPixmap(":/{file}");\n') out.write(f' = QPixmap(":/{file.as_posix()}");\n')
out.write(source_footer) out.write(source_footer)
def writeHeader(out, name, element, indent): def writeHeader(out, name, element, indent):
@ -46,12 +54,11 @@ with open('../src/autogenerated/ResourcesAutogen.hpp', 'w') as out:
elements = {} elements = {}
for file in sorted(image_files): for file in sorted(image_files):
elements_ref = elements elements_ref = elements
directories = str(file).split('/')[:-1] directories = file.as_posix().split('/')[:-1]
filename = file.stem filename = file.stem
for directory in directories: for directory in directories:
if directory not in elements_ref: if directory not in elements_ref:
if directory not in ignored_directories: elements_ref[directory] = {}
elements_ref[directory] = {}
elements_ref = elements_ref[directory] elements_ref = elements_ref[directory]
elements_ref[filename] = filename elements_ref[filename] = filename

View file

@ -1,64 +1,66 @@
<RCC> <RCC>
<qresource prefix="/"> <file>pajaDank.png</file> <qresource prefix="/"> <file>chatterino2.icns</file>
<file>icon.png</file>
<file>emojidata.txt</file>
<file>contributors.txt</file> <file>contributors.txt</file>
<file>error.png</file>
<file>emoji.json</file> <file>emoji.json</file>
<file>emojidata.txt</file>
<file>error.png</file>
<file>icon.ico</file> <file>icon.ico</file>
<file>icon.png</file>
<file>pajaDank.png</file>
<file>tlds.txt</file> <file>tlds.txt</file>
<file>chatterino2.icns</file> <file>avatars/fourtf.png</file>
<file>qss/settings.qss</file> <file>avatars/pajlada.png</file>
<file>__pycache__/_generate_resources.cpython-36.pyc</file> <file>buttons/addSplitDark.png</file>
<file>licenses/fmt_bsd2.txt</file>
<file>licenses/openssl.txt</file>
<file>licenses/pajlada_settings.txt</file>
<file>licenses/qt_lgpl-3.0.txt</file>
<file>licenses/pajlada_signals.txt</file>
<file>licenses/rapidjson.txt</file>
<file>licenses/websocketpp.txt</file>
<file>licenses/boost_boost.txt</file>
<file>licenses/libcommuni_BSD3.txt</file>
<file>settings/aboutlogo.png</file>
<file>settings/behave.svg</file>
<file>settings/accounts.svg</file>
<file>settings/about.svg</file>
<file>settings/notifications.svg</file>
<file>settings/commands.svg</file>
<file>settings/theme.svg</file>
<file>split/up.png</file>
<file>split/left.png</file>
<file>split/move.png</file>
<file>split/right.png</file>
<file>split/down.png</file>
<file>buttons/unban.png</file>
<file>buttons/menuDark.png</file>
<file>buttons/mod.png</file>
<file>buttons/emote.svg</file>
<file>buttons/modModeEnabled2.png</file>
<file>buttons/ban.png</file> <file>buttons/ban.png</file>
<file>buttons/unmod.png</file> <file>buttons/banRed.png</file>
<file>buttons/emote.svg</file>
<file>buttons/emoteDark.svg</file> <file>buttons/emoteDark.svg</file>
<file>buttons/updateError.png</file> <file>buttons/menuDark.png</file>
<file>buttons/menuLight.png</file>
<file>buttons/mod.png</file>
<file>buttons/modModeDisabled.png</file> <file>buttons/modModeDisabled.png</file>
<file>buttons/modModeDisabled2.png</file> <file>buttons/modModeDisabled2.png</file>
<file>buttons/modModeEnabled.png</file> <file>buttons/modModeEnabled.png</file>
<file>buttons/menuLight.png</file> <file>buttons/modModeEnabled2.png</file>
<file>buttons/update.png</file>
<file>buttons/timeout.png</file> <file>buttons/timeout.png</file>
<file>buttons/banRed.png</file> <file>buttons/unban.png</file>
<file>buttons/unmod.png</file>
<file>buttons/update.png</file>
<file>buttons/updateError.png</file>
<file>licenses/boost_boost.txt</file>
<file>licenses/emoji-data-source.txt</file>
<file>licenses/fmt_bsd2.txt</file>
<file>licenses/libcommuni_BSD3.txt</file>
<file>licenses/openssl.txt</file>
<file>licenses/pajlada_settings.txt</file>
<file>licenses/pajlada_signals.txt</file>
<file>licenses/qt_lgpl-3.0.txt</file>
<file>licenses/rapidjson.txt</file>
<file>licenses/websocketpp.txt</file>
<file>qss/settings.qss</file>
<file>settings/about.svg</file>
<file>settings/aboutlogo.png</file>
<file>settings/accounts.svg</file>
<file>settings/behave.svg</file>
<file>settings/commands.svg</file>
<file>settings/emote.svg</file>
<file>settings/notifications.svg</file>
<file>settings/theme.svg</file>
<file>sounds/ping2.wav</file> <file>sounds/ping2.wav</file>
<file>twitch/prime.png</file> <file>split/down.png</file>
<file>twitch/verified.png</file> <file>split/left.png</file>
<file>split/move.png</file>
<file>split/right.png</file>
<file>split/up.png</file>
<file>twitch/admin.png</file> <file>twitch/admin.png</file>
<file>twitch/broadcaster.png</file>
<file>twitch/cheer1.png</file>
<file>twitch/globalmod.png</file>
<file>twitch/moderator.png</file>
<file>twitch/prime.png</file>
<file>twitch/staff.png</file>
<file>twitch/subscriber.png</file> <file>twitch/subscriber.png</file>
<file>twitch/turbo.png</file> <file>twitch/turbo.png</file>
<file>twitch/moderator.png</file> <file>twitch/verified.png</file>
<file>twitch/globalmod.png</file>
<file>twitch/cheer1.png</file>
<file>twitch/broadcaster.png</file>
<file>twitch/staff.png</file>
<file>avatars/fourtf.png</file>
<file>avatars/pajlada.png</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -10,6 +10,7 @@
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp"
#include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/PubsubClient.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchServer.hpp"
@ -56,6 +57,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, taggedUsers(&this->emplace<TaggedUsersController>()) , taggedUsers(&this->emplace<TaggedUsersController>())
, moderationActions(&this->emplace<ModerationActions>()) , moderationActions(&this->emplace<ModerationActions>())
, twitch2(&this->emplace<TwitchServer>()) , twitch2(&this->emplace<TwitchServer>())
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
, logging(&this->emplace<Logging>()) , logging(&this->emplace<Logging>())
{ {

View file

@ -28,7 +28,7 @@ class Settings;
class Fonts; class Fonts;
class Resources2; class Resources2;
class Toasts; class Toasts;
class ChatterinoBadges;
class Application class Application
{ {
@ -65,6 +65,7 @@ public:
TaggedUsersController *const taggedUsers{}; TaggedUsersController *const taggedUsers{};
ModerationActions *const moderationActions{}; ModerationActions *const moderationActions{};
TwitchServer *const twitch2{}; TwitchServer *const twitch2{};
ChatterinoBadges *const chatterinoBadges{};
/*[[deprecated]]*/ Logging *const logging{}; /*[[deprecated]]*/ Logging *const logging{};

View file

@ -6,6 +6,7 @@ Resources2::Resources2()
{ {
this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); this->avatars.fourtf = QPixmap(":/avatars/fourtf.png");
this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); this->avatars.pajlada = QPixmap(":/avatars/pajlada.png");
this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png");
this->buttons.ban = QPixmap(":/buttons/ban.png"); this->buttons.ban = QPixmap(":/buttons/ban.png");
this->buttons.banRed = QPixmap(":/buttons/banRed.png"); this->buttons.banRed = QPixmap(":/buttons/banRed.png");
this->buttons.menuDark = QPixmap(":/buttons/menuDark.png"); this->buttons.menuDark = QPixmap(":/buttons/menuDark.png");

View file

@ -3,8 +3,7 @@
namespace chatterino { namespace chatterino {
class Resources2 : public Singleton class Resources2 : public Singleton {
{
public: public:
Resources2(); Resources2();
@ -13,6 +12,7 @@ public:
QPixmap pajlada; QPixmap pajlada;
} avatars; } avatars;
struct { struct {
QPixmap addSplitDark;
QPixmap ban; QPixmap ban;
QPixmap banRed; QPixmap banRed;
QPixmap menuDark; QPixmap menuDark;

View file

@ -143,7 +143,8 @@ void Channel::addOrReplaceTimeout(MessagePtr message)
if (s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout}) && if (s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout}) &&
s->loginName == message->timeoutUser) { s->loginName == message->timeoutUser) {
// FOURTF: disabled for now // FOURTF: disabled for now
// s->flags.EnableFlag(MessageFlag::Disabled); // PAJLADA: Shitty solution described in Message.hpp
s->flags.set(MessageFlag::Disabled);
} }
} }

View file

@ -124,7 +124,7 @@ public:
rowItem.items[column]->setData(value, role); rowItem.items[column]->setData(value, role);
if (rowItem.isCustomRow) { if (rowItem.isCustomRow) {
this->customRowSetData(rowItem.items, column, value, role); this->customRowSetData(rowItem.items, column, value, role, row);
} else { } else {
int vecRow = this->getVectorIndexFromModelIndex(row); int vecRow = this->getVectorIndexFromModelIndex(row);
this->vector_->removeItem(vecRow, this); this->vector_->removeItem(vecRow, this);
@ -230,7 +230,8 @@ protected:
} }
virtual void customRowSetData(const std::vector<QStandardItem *> &row, virtual void customRowSetData(const std::vector<QStandardItem *> &row,
int column, const QVariant &value, int role) int column, const QVariant &value, int role,
int rowIndex)
{ {
} }

View file

@ -36,36 +36,67 @@ void HighlightModel::getRowFromItem(const HighlightPhrase &item,
void HighlightModel::afterInit() void HighlightModel::afterInit()
{ {
std::vector<QStandardItem *> row = this->createRow(); std::vector<QStandardItem *> usernameRow = this->createRow();
setBoolItem(row[0], getSettings()->enableHighlightsSelf.getValue(), true, setBoolItem(usernameRow[0], getSettings()->enableSelfHighlight.getValue(),
true, false);
usernameRow[0]->setData("Your username (automatic)", Qt::DisplayRole);
setBoolItem(usernameRow[1],
getSettings()->enableSelfHighlightTaskbar.getValue(), true,
false); false);
row[0]->setData("Your username (automatic)", Qt::DisplayRole); setBoolItem(usernameRow[2],
setBoolItem(row[1], getSettings()->enableHighlightTaskbar.getValue(), true, getSettings()->enableSelfHighlightSound.getValue(), true,
false); false);
setBoolItem(row[2], getSettings()->enableHighlightSound.getValue(), true, usernameRow[3]->setFlags(0);
this->insertCustomRow(usernameRow, 0);
std::vector<QStandardItem *> whisperRow = this->createRow();
setBoolItem(whisperRow[0], getSettings()->enableWhisperHighlight.getValue(),
true, false);
whisperRow[0]->setData("Whispers", Qt::DisplayRole);
setBoolItem(whisperRow[1],
getSettings()->enableWhisperHighlightTaskbar.getValue(), true,
false); false);
row[3]->setFlags(0); setBoolItem(whisperRow[2],
this->insertCustomRow(row, 0); getSettings()->enableWhisperHighlightSound.getValue(), true,
false);
whisperRow[3]->setFlags(0);
this->insertCustomRow(whisperRow, 1);
} }
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row, void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
int column, const QVariant &value, int column, const QVariant &value,
int role) int role, int rowIndex)
{ {
switch (column) { switch (column) {
case 0: { case 0: {
if (role == Qt::CheckStateRole) { if (role == Qt::CheckStateRole) {
getSettings()->enableHighlightsSelf.setValue(value.toBool()); if (rowIndex == 0) {
getSettings()->enableSelfHighlight.setValue(value.toBool());
} else if (rowIndex == 1) {
getSettings()->enableWhisperHighlight.setValue(
value.toBool());
}
} }
} break; } break;
case 1: { case 1: {
if (role == Qt::CheckStateRole) { if (role == Qt::CheckStateRole) {
getSettings()->enableHighlightTaskbar.setValue(value.toBool()); if (rowIndex == 0) {
getSettings()->enableSelfHighlightTaskbar.setValue(
value.toBool());
} else if (rowIndex == 1) {
getSettings()->enableWhisperHighlightTaskbar.setValue(
value.toBool());
}
} }
} break; } break;
case 2: { case 2: {
if (role == Qt::CheckStateRole) { if (role == Qt::CheckStateRole) {
getSettings()->enableHighlightSound.setValue(value.toBool()); if (rowIndex == 0) {
getSettings()->enableSelfHighlightSound.setValue(
value.toBool());
} else if (rowIndex == 1) {
getSettings()->enableWhisperHighlightSound.setValue(
value.toBool());
}
} }
} break; } break;
case 3: { case 3: {

View file

@ -26,8 +26,8 @@ protected:
virtual void afterInit() override; virtual void afterInit() override;
virtual void customRowSetData(const std::vector<QStandardItem *> &row, virtual void customRowSetData(const std::vector<QStandardItem *> &row,
int column, const QVariant &value, int column, const QVariant &value, int role,
int role) override; int rowIndex) override;
friend class HighlightController; friend class HighlightController;
}; };

View file

@ -310,6 +310,14 @@ void Image::load()
return Success; return Success;
}); });
req.onError([that = this, weak = weakOf(this)](auto result) -> bool {
auto shared = weak.lock();
if (!shared) return false;
shared->empty_ = true;
return true;
});
req.execute(); req.execute();
} }

View file

@ -34,7 +34,13 @@ struct Message : boost::noncopyable {
Message(); Message();
~Message(); ~Message();
MessageFlags flags; // Making this a mutable means that we can update a messages flags,
// while still keeping Message constant. This means that a message's flag
// can be updated without the renderer being made aware, which might be bad.
// This is a temporary effort until we can figure out what the right
// const-correct way to deal with this is.
// This might bring race conditions with it
mutable MessageFlags flags;
QTime parseTime; QTime parseTime;
QString id; QString id;
QString searchText; QString searchText;

View file

@ -61,6 +61,12 @@ MessageElementFlags MessageElement::getFlags() const
return this->flags_; return this->flags_;
} }
MessageElement *MessageElement::updateLink()
{
this->linkChanged.invoke();
return this;
}
// IMAGE // IMAGE
ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags) ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags)
: MessageElement(flags) : MessageElement(flags)
@ -152,6 +158,15 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
color, this->style_, container.getScale())) color, this->style_, container.getScale()))
->setLink(this->getLink()); ->setLink(this->getLink());
e->setTrailingSpace(trailingSpace); e->setTrailingSpace(trailingSpace);
// If URL link was changed,
// Should update it in MessageLayoutElement too!
if (this->getLink().type == Link::Url) {
this->linkChanged.connect(
[this, e]() {
e->setLink(this->getLink());
});
}
return e; return e;
}; };

View file

@ -12,6 +12,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <pajlada/signals/signalholder.hpp>
namespace chatterino { namespace chatterino {
class Channel; class Channel;
@ -128,6 +129,7 @@ public:
const Link &getLink() const; const Link &getLink() const;
bool hasTrailingSpace() const; bool hasTrailingSpace() const;
MessageElementFlags getFlags() const; MessageElementFlags getFlags() const;
MessageElement *updateLink();
virtual void addToContainer(MessageLayoutContainer &container, virtual void addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags) = 0; MessageElementFlags flags) = 0;
@ -135,6 +137,7 @@ public:
protected: protected:
MessageElement(MessageElementFlags flags); MessageElement(MessageElementFlags flags);
bool trailingSpace = true; bool trailingSpace = true;
pajlada::Signals::NoArgSignal linkChanged;
private: private:
Link link_; Link link_;

View file

@ -0,0 +1,48 @@
#include "providers/LinkResolver.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "messages/Link.hpp"
#include "singletons/Settings.hpp"
#include <QString>
namespace chatterino {
void LinkResolver::getLinkInfo(const QString url,
std::function<void(QString, Link)> successCallback)
{
QString requestUrl("https://braize.pajlada.com/chatterino/link_resolver/" +
QUrl::toPercentEncoding(url, "", "/:"));
NetworkRequest request(requestUrl);
request.setCaller(QThread::currentThread());
request.setTimeout(30000);
request.onSuccess([successCallback, url](auto result) mutable -> Outcome {
auto root = result.parseJson();
auto statusCode = root.value("status").toInt();
QString response = QString();
QString linkString = url;
if (statusCode == 200) {
response = root.value("tooltip").toString();
if (getSettings()->enableUnshortLinks) {
linkString = root.value("link").toString();
}
} else {
response = root.value("message").toString();
}
successCallback(QUrl::fromPercentEncoding(response.toUtf8()), Link(Link::Url, linkString));
return Success;
});
request.onError([successCallback, url](auto result) {
successCallback("No link info found", Link(Link::Url, url));
return true;
});
request.execute();
}
} // namespace chatterino

View file

@ -0,0 +1,19 @@
#pragma once
#include <QString>
#include <functional>
#include "messages/Link.hpp"
namespace chatterino {
class LinkResolver
{
public:
static void getLinkInfo(const QString url,
std::function<void(QString, Link)> callback);
private:
};
} // namespace chatterino

View file

@ -5,8 +5,18 @@
#include <QJsonValue> #include <QJsonValue>
#include <QThread> #include <QThread>
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "messages/Emote.hpp"
#include <QUrl>
#include <map>
namespace chatterino { namespace chatterino {
void ChatterinoBadges::initialize(Settings &settings, Paths &paths)
{
this->loadChatterinoBadges();
}
ChatterinoBadges::ChatterinoBadges() ChatterinoBadges::ChatterinoBadges()
{ {
@ -14,41 +24,41 @@ ChatterinoBadges::ChatterinoBadges()
boost::optional<EmotePtr> ChatterinoBadges::getBadge(const UserName &username) boost::optional<EmotePtr> ChatterinoBadges::getBadge(const UserName &username)
{ {
auto it = badgeMap.find(username.string);
if (it != badgeMap.end()) {
return emotes[it->second];
}
return boost::none; return boost::none;
// return this->badges.access()->get(username);
} }
void ChatterinoBadges::loadChatterinoBadges() void ChatterinoBadges::loadChatterinoBadges()
{ {
// static QString url("https://fourtf.com/chatterino/badges.json"); static QUrl url("https://fourtf.com/chatterino/badges.json");
// NetworkRequest req(url); NetworkRequest req(url);
// req.setCaller(QThread::currentThread()); req.setCaller(QThread::currentThread());
// req.onSuccess([this](auto result) { req.onSuccess([this](auto result) -> Outcome {
// auto jsonRoot = result.parseJson(); auto jsonRoot = result.parseJson();
// auto badges = this->badges.access(); int index = 0;
// auto replacement = badges->makeReplacment(); for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray()) {
auto jsonBadge = jsonBadge_.toObject();
auto emote = Emote{
EmoteName{}, ImageSet{Url{jsonBadge.value("image").toString()}},
Tooltip{jsonBadge.value("tooltip").toString()}, Url{}};
// for (auto jsonBadge_ : jsonRoot.value("badges").toArray()) { emotes.push_back(std::make_shared<const Emote>(std::move(emote)));
// auto jsonBadge = jsonBadge_.toObject();
// auto emote = Emote{ for (const auto &user : jsonBadge.value("users").toArray()) {
// EmoteName{}, badgeMap[user.toString()] = index;
// ImageSet{Url{jsonBadge.value("image").toString()}}, }
// Tooltip{jsonBadge.value("tooltip").toString()}, Url{}}; ++index;
}
// for (auto jsonUser : jsonBadge.value("users").toArray()) { return Success;
// replacement.add(UserName{jsonUser.toString()}, });
// std::move(emote));
// }
// }
// replacement.apply(); req.execute();
// return Success;
//});
// req.execute();
} }
} // namespace chatterino } // namespace chatterino

View file

@ -1,25 +1,30 @@
#pragma once #pragma once
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <common/Singleton.hpp>
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include <map>
#include <vector>
namespace chatterino { namespace chatterino {
struct Emote; struct Emote;
using EmotePtr = std::shared_ptr<const Emote>; using EmotePtr = std::shared_ptr<const Emote>;
class ChatterinoBadges class ChatterinoBadges : public Singleton
{ {
public: public:
virtual void initialize(Settings &settings, Paths &paths) override;
ChatterinoBadges(); ChatterinoBadges();
boost::optional<EmotePtr> getBadge(const UserName &username); boost::optional<EmotePtr> getBadge(const UserName &username);
private: private:
void loadChatterinoBadges(); void loadChatterinoBadges();
std::map<QString, int> badgeMap;
// UniqueAccess<EmoteCache<UserName>> badges; std::vector<EmotePtr> emotes;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -66,8 +66,8 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
builder->flags.unset(MessageFlag::Highlighted); builder->flags.unset(MessageFlag::Highlighted);
} }
auto highlighted = builder->flags.has(MessageFlag::Highlighted);
auto msg = builder.build(); auto msg = builder.build();
auto highlighted = msg->flags.has(MessageFlag::Highlighted);
if (!isSub) { if (!isSub) {
if (highlighted) { if (highlighted) {
@ -221,10 +221,10 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
false); false);
if (!builder.isIgnored()) { if (!builder.isIgnored()) {
app->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
MessagePtr _message = builder.build(); MessagePtr _message = builder.build();
app->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
if (_message->flags.has(MessageFlag::Highlighted)) { if (_message->flags.has(MessageFlag::Highlighted)) {
app->twitch.server->mentionsChannel->addMessage(_message); app->twitch.server->mentionsChannel->addMessage(_message);
} }

View file

@ -633,12 +633,13 @@ void TwitchChannel::refreshBadges()
auto jsonVersion = jsonVersion_->toObject(); auto jsonVersion = jsonVersion_->toObject();
auto emote = std::make_shared<Emote>(Emote{ auto emote = std::make_shared<Emote>(Emote{
EmoteName{}, EmoteName{},
ImageSet{Image::fromUrl( ImageSet{
{jsonVersion["image_url_1x"].toString()}), Image::fromUrl({jsonVersion["image_url_1x"].toString()},
Image::fromUrl( 1),
{jsonVersion["image_url_2x"].toString()}), Image::fromUrl({jsonVersion["image_url_2x"].toString()},
Image::fromUrl( .5),
{jsonVersion["image_url_4x"].toString()})}, Image::fromUrl({jsonVersion["image_url_4x"].toString()},
.25)},
Tooltip{jsonRoot["description"].toString()}, Tooltip{jsonRoot["description"].toString()},
Url{jsonVersion["clickURL"].toString()}}); Url{jsonVersion["clickURL"].toString()}});

View file

@ -6,6 +6,8 @@
#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnoreController.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "providers/LinkResolver.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
@ -268,12 +270,29 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
link = Link(Link::Url, linkString); link = Link(Link::Url, linkString);
textColor = MessageColor(MessageColor::Link); textColor = MessageColor(MessageColor::Link);
this->emplace<TextElement>(lowercaseLinkString, auto linkMELowercase =
MessageElementFlag::LowercaseLink, textColor) this->emplace<TextElement>(lowercaseLinkString,
->setLink(link); MessageElementFlag::LowercaseLink,
this->emplace<TextElement>(string, MessageElementFlag::OriginalLink, textColor)
textColor) ->setLink(link);
->setLink(link); auto linkMEOriginal =
this->emplace<TextElement>(string, MessageElementFlag::OriginalLink,
textColor)
->setLink(link);
LinkResolver::getLinkInfo(
linkString, [linkMELowercase, linkMEOriginal, linkString]
(QString tooltipText, Link originalLink) {
if (!tooltipText.isEmpty()) {
linkMELowercase->setTooltip(tooltipText);
linkMEOriginal->setTooltip(tooltipText);
}
if (originalLink.value != linkString &&
!originalLink.value.isEmpty()) {
linkMELowercase->setLink(originalLink)->updateLink();
linkMEOriginal->setLink(originalLink)->updateLink();
}
});
} }
// if (!linkString.isEmpty()) { // if (!linkString.isEmpty()) {
@ -507,10 +526,10 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
std::vector<HighlightPhrase> userHighlights = std::vector<HighlightPhrase> userHighlights =
app->highlights->highlightedUsers.getVector(); app->highlights->highlightedUsers.getVector();
if (getSettings()->enableHighlightsSelf && currentUsername.size() > 0) { if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) {
HighlightPhrase selfHighlight( HighlightPhrase selfHighlight(
currentUsername, getSettings()->enableHighlightTaskbar, currentUsername, getSettings()->enableSelfHighlightTaskbar,
getSettings()->enableHighlightSound, false); getSettings()->enableSelfHighlightSound, false);
activeHighlights.emplace_back(std::move(selfHighlight)); activeHighlights.emplace_back(std::move(selfHighlight));
} }
@ -564,6 +583,15 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
} }
} }
} }
if (this->args.isReceivedWhisper &&
getSettings()->enableWhisperHighlight) {
if (getSettings()->enableWhisperHighlightTaskbar) {
doAlert = true;
}
if (getSettings()->enableWhisperHighlightSound) {
playSound = true;
}
}
this->message().flags.set(MessageFlag::Highlighted, doHighlight); this->message().flags.set(MessageFlag::Highlighted, doHighlight);
@ -769,20 +797,12 @@ void TwitchMessageBuilder::appendTwitchBadges()
void TwitchMessageBuilder::appendChatterinoBadges() void TwitchMessageBuilder::appendChatterinoBadges()
{ {
// auto app = getApp(); auto chatterinoBadgePtr =
getApp()->chatterinoBadges->getBadge({this->userName});
// auto &badges = app->resources->chatterinoBadges; if (chatterinoBadgePtr) {
// auto it = badges.find(this->userName.toStdString()); this->emplace<EmoteElement>(*chatterinoBadgePtr,
MessageElementFlag::BadgeChatterino);
// if (it == badges.end()) { }
// return;
// }
// const auto badge = it->second;
// this->emplace<ImageElement>(badge->image,
// MessageElementFlag::BadgeChatterino)
// ->setTooltip(QString::fromStdString(badge->tooltip));
} }
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)

View file

@ -29,7 +29,6 @@ public:
"/appearance/enableAnimationsWhenFocused", false}; "/appearance/enableAnimationsWhenFocused", false};
QStringSetting timestampFormat = {"/appearance/messages/timestampFormat", QStringSetting timestampFormat = {"/appearance/messages/timestampFormat",
"h:mm"}; "h:mm"};
BoolSetting showBadges = {"/appearance/messages/showBadges", true};
BoolSetting showLastMessageIndicator = { BoolSetting showLastMessageIndicator = {
"/appearance/messages/showLastMessageIndicator", false}; "/appearance/messages/showLastMessageIndicator", false};
IntSetting lastMessagePattern = {"/appearance/messages/lastMessagePattern", IntSetting lastMessagePattern = {"/appearance/messages/lastMessagePattern",
@ -67,6 +66,16 @@ public:
// BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame",
// false}; // false};
// Badges
BoolSetting showBadgesGlobalAuthority = {
"/appearance/badges/GlobalAuthority", true};
BoolSetting showBadgesChannelAuthority = {
"/appearance/badges/ChannelAuthority", true};
BoolSetting showBadgesSubscription = {"/appearance/badges/subscription",
true};
BoolSetting showBadgesVanity = {"/appearance/badges/vanity", true};
BoolSetting showBadgesChatterino = {"/appearance/badges/chatterino", true};
/// Behaviour /// Behaviour
BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages",
true}; true};
@ -103,6 +112,8 @@ public:
/// Links /// Links
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
BoolSetting enableLinkInfoTooltip = {"/links/linkInfoTooltip", false};
BoolSetting enableUnshortLinks = {"/links/unshortLinks", false};
BoolSetting enableLowercaseLink = {"/links/linkLowercase", true}; BoolSetting enableLowercaseLink = {"/links/linkLowercase", true};
/// Ingored Users /// Ingored Users
@ -114,12 +125,19 @@ public:
/// Highlighting /// Highlighting
// BoolSetting enableHighlights = {"/highlighting/enabled", true}; // BoolSetting enableHighlights = {"/highlighting/enabled", true};
BoolSetting enableHighlightsSelf = {"/highlighting/nameIsHighlightKeyword",
true};
BoolSetting enableHighlightSound = {"/highlighting/enableSound", true};
BoolSetting enableHighlightTaskbar = {"/highlighting/enableTaskbarFlashing",
true};
BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false}; BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false};
BoolSetting enableSelfHighlight = {
"/highlighting/selfHighlight/nameIsHighlightKeyword", true};
BoolSetting enableSelfHighlightSound = {
"/highlighting/selfHighlight/enableSound", true};
BoolSetting enableSelfHighlightTaskbar = {
"/highlighting/selfHighlight/enableTaskbarFlashing", true};
BoolSetting enableWhisperHighlight = {
"/highlighting/whisperHighlight/whispersHighlighted", true};
BoolSetting enableWhisperHighlightSound = {
"/highlighting/whisperHighlight/enableSound", false};
BoolSetting enableWhisperHighlightTaskbar = {
"/highlighting/whisperHighlight/enableTaskbarFlashing", false};
/// Logging /// Logging
BoolSetting enableLogging = {"/logging/enabled", false}; BoolSetting enableLogging = {"/logging/enabled", false};

View file

@ -69,7 +69,11 @@ WindowManager::WindowManager()
auto settings = getSettings(); auto settings = getSettings();
this->wordFlagsListener_.addSetting(settings->showTimestamps); this->wordFlagsListener_.addSetting(settings->showTimestamps);
this->wordFlagsListener_.addSetting(settings->showBadges); this->wordFlagsListener_.addSetting(settings->showBadgesGlobalAuthority);
this->wordFlagsListener_.addSetting(settings->showBadgesChannelAuthority);
this->wordFlagsListener_.addSetting(settings->showBadgesSubscription);
this->wordFlagsListener_.addSetting(settings->showBadgesVanity);
this->wordFlagsListener_.addSetting(settings->showBadgesChatterino);
this->wordFlagsListener_.addSetting(settings->enableBttvEmotes); this->wordFlagsListener_.addSetting(settings->enableBttvEmotes);
this->wordFlagsListener_.addSetting(settings->enableEmojis); this->wordFlagsListener_.addSetting(settings->enableEmojis);
this->wordFlagsListener_.addSetting(settings->enableFfzEmotes); this->wordFlagsListener_.addSetting(settings->enableFfzEmotes);
@ -114,7 +118,15 @@ void WindowManager::updateWordTypeMask()
: MEF::BitsStatic); : MEF::BitsStatic);
// badges // badges
flags.set(settings->showBadges ? MEF::Badges : MEF::None); flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority
: MEF::None);
flags.set(settings->showBadgesChannelAuthority ? MEF::BadgeChannelAuthority
: MEF::None);
flags.set(settings->showBadgesSubscription ? MEF::BadgeSubscription
: MEF::None);
flags.set(settings->showBadgesVanity ? MEF::BadgeVanity : MEF::None);
flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino
: MEF::None);
// username // username
flags.set(MEF::Username); flags.set(MEF::Username);

View file

@ -61,9 +61,9 @@ protected:
private: private:
struct Item { struct Item {
NotebookTab *tab; NotebookTab *tab{};
QWidget *page; QWidget *page{};
QWidget *selectedWidget = nullptr; QWidget *selectedWidget{};
}; };
bool containsPage(QWidget *page); bool containsPage(QWidget *page);

View file

@ -55,6 +55,11 @@ void Scrollbar::unpauseHighlights()
this->highlightsPaused_ = false; this->highlightsPaused_ = false;
} }
void Scrollbar::clearHighlights()
{
this->highlights_.clear();
}
LimitedQueueSnapshot<ScrollbarHighlight> Scrollbar::getHighlightSnapshot() LimitedQueueSnapshot<ScrollbarHighlight> Scrollbar::getHighlightSnapshot()
{ {
if (!this->highlightsPaused_) { if (!this->highlightsPaused_) {

View file

@ -27,6 +27,7 @@ public:
void pauseHighlights(); void pauseHighlights();
void unpauseHighlights(); void unpauseHighlights();
void clearHighlights();
void scrollToBottom(bool animate = false); void scrollToBottom(bool animate = false);
bool isAtBottom() const; bool isAtBottom() const;

View file

@ -87,6 +87,11 @@ void TooltipWidget::setText(QString text)
this->displayText_->setText(text); this->displayText_->setText(text);
} }
void TooltipWidget::setWordWrap(bool wrap)
{
this->displayText_->setWordWrap(wrap);
}
void TooltipWidget::changeEvent(QEvent *) void TooltipWidget::changeEvent(QEvent *)
{ {
// clear parents event // clear parents event

View file

@ -19,6 +19,7 @@ public:
virtual ~TooltipWidget() override; virtual ~TooltipWidget() override;
void setText(QString text); void setText(QString text);
void setWordWrap(bool wrap);
#ifdef USEWINSDK #ifdef USEWINSDK
void raise(); void raise();

View file

@ -41,7 +41,8 @@ Window::Window(WindowType type)
this->addShortcuts(); this->addShortcuts();
this->addLayout(); this->addLayout();
getApp()->accounts->twitch.currentUserChanged.connect( this->signalHolder_.managedConnect(
getApp()->accounts->twitch.currentUserChanged,
[this] { this->onAccountSelected(); }); [this] { this->onAccountSelected(); });
this->onAccountSelected(); this->onAccountSelected();

View file

@ -29,11 +29,17 @@ namespace {
builder->flags.set(MessageFlag::Centered); builder->flags.set(MessageFlag::Centered);
builder->flags.set(MessageFlag::DisableCompactEmotes); builder->flags.set(MessageFlag::DisableCompactEmotes);
for (const auto &emote : map) { if (!map.empty()) {
builder for (const auto &emote : map) {
.emplace<EmoteElement>(emote.second, builder
MessageElementFlag::AlwaysShow) .emplace<EmoteElement>(emote.second,
->setLink(Link(Link::InsertText, emote.first.string)); MessageElementFlag::AlwaysShow)
->setLink(Link(Link::InsertText, emote.first.string));
}
} else {
builder.emplace<TextElement>("no emotes available",
MessageElementFlag::Text,
MessageColor::System);
} }
return builder.release(); return builder.release();
@ -106,7 +112,7 @@ void EmotePopup::loadChannel(ChannelPtr _channel)
{ {
BenchmarkGuard guard("loadChannel"); BenchmarkGuard guard("loadChannel");
this->setWindowTitle("Emotes from " + _channel->getName()); this->setWindowTitle("Emotes in #" + _channel->getName());
auto twitchChannel = dynamic_cast<TwitchChannel *>(_channel.get()); auto twitchChannel = dynamic_cast<TwitchChannel *>(_channel.get());
if (twitchChannel == nullptr) return; if (twitchChannel == nullptr) return;
@ -139,6 +145,16 @@ void EmotePopup::loadChannel(ChannelPtr _channel)
this->globalEmotesView_->setChannel(globalChannel); this->globalEmotesView_->setChannel(globalChannel);
this->subEmotesView_->setChannel(subChannel); this->subEmotesView_->setChannel(subChannel);
this->channelEmotesView_->setChannel(channelChannel); this->channelEmotesView_->setChannel(channelChannel);
if (subChannel->getMessageSnapshot().getLength() == 0) {
MessageBuilder builder;
builder->flags.set(MessageFlag::Centered);
builder->flags.set(MessageFlag::DisableCompactEmotes);
builder.emplace<TextElement>("no subscription emotes available",
MessageElementFlag::Text,
MessageColor::System);
subChannel->addMessage(builder.release());
}
} }
void EmotePopup::loadEmojis() void EmotePopup::loadEmojis()

View file

@ -35,6 +35,7 @@ SettingsDialog::SettingsDialog()
this->scaleChangedEvent(this->getScale()); this->scaleChangedEvent(this->getScale());
this->overrideBackgroundColor_ = QColor("#282828"); this->overrideBackgroundColor_ = QColor("#282828");
this->themeChangedEvent();
} }
void SettingsDialog::initUi() void SettingsDialog::initUi()
@ -187,7 +188,7 @@ void SettingsDialog::themeChangedEvent()
BaseWindow::themeChangedEvent(); BaseWindow::themeChangedEvent();
QPalette palette; QPalette palette;
palette.setColor(QPalette::Background, QColor("#444")); palette.setColor(QPalette::Background, QColor("#222"));
this->setPalette(palette); this->setPalette(palette);
} }

View file

@ -62,6 +62,18 @@ bool Button::getEnable() const
return this->enabled_; return this->enabled_;
} }
void Button::setEnableMargin(bool value)
{
this->enableMargin_ = value;
this->update();
}
bool Button::getEnableMargin() const
{
return this->enableMargin_;
}
qreal Button::getCurrentDimAmount() const qreal Button::getCurrentDimAmount() const
{ {
return this->dimPixmap_ && !this->mouseOver_ ? 0.7 : 1; return this->dimPixmap_ && !this->mouseOver_ ? 0.7 : 1;
@ -105,7 +117,7 @@ void Button::paintEvent(QPaintEvent *)
} }
QRect rect = this->rect(); QRect rect = this->rect();
int s = int(6 * this->getScale()); int s = this->enableMargin_ ? int(6 * this->getScale()) : 0;
rect.moveLeft(s); rect.moveLeft(s);
rect.setRight(rect.right() - s - s); rect.setRight(rect.right() - s - s);

View file

@ -41,6 +41,9 @@ public:
void setEnable(bool value); void setEnable(bool value);
bool getEnable() const; bool getEnable() const;
void setEnableMargin(bool value);
bool getEnableMargin() const;
void setBorderColor(const QColor &color); void setBorderColor(const QColor &color);
const QColor &getBorderColor() const; const QColor &getBorderColor() const;
@ -73,6 +76,7 @@ private:
QColor borderColor_{}; QColor borderColor_{};
QPixmap pixmap_{}; QPixmap pixmap_{};
bool dimPixmap_{true}; bool dimPixmap_{true};
bool enableMargin_{true};
QPoint mousePos_{}; QPoint mousePos_{};
double hoverMultiplier_{0.0}; double hoverMultiplier_{0.0};
QTimer effectTimer_{}; QTimer effectTimer_{};

View file

@ -305,6 +305,7 @@ void ChannelView::clearMessages()
{ {
// Clear all stored messages in this chat widget // Clear all stored messages in this chat widget
this->messages.clear(); this->messages.clear();
this->scrollBar_->clearHighlights();
// Layout chat widget messages, and force an update regardless if there are // Layout chat widget messages, and force an update regardless if there are
// no messages // no messages
@ -870,11 +871,15 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
return; return;
} }
const auto &tooltip = hoverLayoutElement->getCreator().getTooltip(); const auto &tooltip = hoverLayoutElement->getCreator().getTooltip();
bool isLinkValid = hoverLayoutElement->getLink().isValid();
if (tooltip.isEmpty()) { if (tooltip.isEmpty()) {
tooltipWidget->hide(); tooltipWidget->hide();
} else if (isLinkValid && !getSettings()->enableLinkInfoTooltip) {
tooltipWidget->hide();
} else { } else {
tooltipWidget->moveTo(this, event->globalPos()); tooltipWidget->moveTo(this, event->globalPos());
tooltipWidget->setWordWrap(isLinkValid);
tooltipWidget->setText(tooltip); tooltipWidget->setText(tooltip);
tooltipWidget->adjustSize(); tooltipWidget->adjustSize();
tooltipWidget->show(); tooltipWidget->show();
@ -882,7 +887,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
} }
// check if word has a link // check if word has a link
if (hoverLayoutElement->getLink().isValid()) { if (isLinkValid) {
this->setCursor(Qt::PointingHandCursor); this->setCursor(Qt::PointingHandCursor);
} else { } else {
this->setCursor(Qt::ArrowCursor); this->setCursor(Qt::ArrowCursor);

View file

@ -313,6 +313,8 @@ void NotebookTab::paintEvent(QPaintEvent *)
if (this->shouldDrawXButton()) { if (this->shouldDrawXButton()) {
QRect xRect = this->getXRect(); QRect xRect = this->getXRect();
if (!xRect.isNull()) { if (!xRect.isNull()) {
if (this->selected_) xRect.moveTop(xRect.top() - 1);
painter.setBrush(QColor("#fff")); painter.setBrush(QColor("#fff"));
if (this->mouseOverX_) { if (this->mouseOverX_) {
@ -474,7 +476,7 @@ QRect NotebookTab::getXRect()
float s = this->getScale(); float s = this->getScale();
return QRect(this->width() - static_cast<int>(20 * s), return QRect(this->width() - static_cast<int>(20 * s),
static_cast<int>(6 * s), static_cast<int>(16 * s), static_cast<int>(9 * s), static_cast<int>(16 * s),
static_cast<int>(16 * s)); static_cast<int>(16 * s));
} }

View file

@ -50,6 +50,12 @@ FeelPage::FeelPage()
form->addRow("Links:", form->addRow("Links:",
this->createCheckBox("Open links only on double click", this->createCheckBox("Open links only on double click",
getSettings()->linksDoubleClickOnly)); getSettings()->linksDoubleClickOnly));
form->addRow("",
this->createCheckBox("Show link info in tooltips",
getSettings()->enableLinkInfoTooltip));
form->addRow("",
this->createCheckBox("Auto unshort links (requires restart)",
getSettings()->enableUnshortLinks));
} }
layout->addSpacing(16); layout->addSpacing(16);

View file

@ -41,6 +41,8 @@ KeyboardSettingsPage::KeyboardSettingsPage()
form->addRow(new QLabel("Ctrl + R"), new QLabel("Change channel")); form->addRow(new QLabel("Ctrl + R"), new QLabel("Change channel"));
form->addRow(new QLabel("Ctrl + F"), form->addRow(new QLabel("Ctrl + F"),
new QLabel("Search in current channel")); new QLabel("Search in current channel"));
form->addRow(new QLabel("Ctrl + E"),
new QLabel("Open Emote menu"));
} }
} // namespace chatterino } // namespace chatterino

View file

@ -59,6 +59,7 @@ void LookPage::initializeUi()
this->addMessageTab(tabs.appendTab(new QVBoxLayout, "Messages")); this->addMessageTab(tabs.appendTab(new QVBoxLayout, "Messages"));
this->addEmoteTab(tabs.appendTab(new QVBoxLayout, "Emotes")); this->addEmoteTab(tabs.appendTab(new QVBoxLayout, "Emotes"));
this->addSplitHeaderTab(tabs.appendTab(new QVBoxLayout, "Split header")); this->addSplitHeaderTab(tabs.appendTab(new QVBoxLayout, "Split header"));
this->addBadgesTab(tabs.appendTab(new QVBoxLayout, "Badges"));
layout->addStretch(1); layout->addStretch(1);
@ -145,10 +146,6 @@ void LookPage::addMessageTab(LayoutCreator<QVBoxLayout> layout)
box->addStretch(1); box->addStretch(1);
} }
// badges
layout.append(
this->createCheckBox("Show badges", getSettings()->showBadges));
// -- // --
layout.emplace<Line>(false); layout.emplace<Line>(false);
@ -270,16 +267,57 @@ void LookPage::addEmoteTab(LayoutCreator<QVBoxLayout> layout)
void LookPage::addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout) void LookPage::addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout)
{ {
layout.append(this->createCheckBox("Show viewer count",
getSettings()->showViewerCount));
layout.append(this->createCheckBox("Show title", getSettings()->showTitle));
layout.append(this->createCheckBox("Show game", getSettings()->showGame));
layout.append( layout.append(
this->createCheckBox("Show uptime", getSettings()->showUptime)); this->createCheckBox("Show uptime", getSettings()->showUptime));
layout.append(this->createCheckBox("Show viewer count",
getSettings()->showViewerCount));
layout.append(this->createCheckBox("Show game", getSettings()->showGame));
layout.append(this->createCheckBox("Show title", getSettings()->showTitle));
layout->addStretch(1); layout->addStretch(1);
} }
void LookPage::addBadgesTab(LayoutCreator<QVBoxLayout> layout)
{
// layout.append(
// this->createCheckBox(("Show all badges"), getSettings()->showBadges));
auto fastSelection = layout.emplace<QHBoxLayout>();
{
auto addAll = fastSelection.emplace<QPushButton>("Enable all");
QObject::connect(addAll.getElement(), &QPushButton::clicked, this, [] {
getSettings()->showBadgesGlobalAuthority = true;
getSettings()->showBadgesChannelAuthority = true;
getSettings()->showBadgesSubscription = true;
getSettings()->showBadgesVanity = true;
getSettings()->showBadgesChatterino = true;
});
auto removeAll = fastSelection.emplace<QPushButton>("Disable all");
QObject::connect(removeAll.getElement(), &QPushButton::clicked, this,
[] {
getSettings()->showBadgesGlobalAuthority = false;
getSettings()->showBadgesChannelAuthority = false;
getSettings()->showBadgesSubscription = false;
getSettings()->showBadgesVanity = false;
getSettings()->showBadgesChatterino = false;
});
}
layout.emplace<Line>(false);
layout.append(this->createCheckBox(
("Show authorty badges (staff, admin, turbo, etc)"),
getSettings()->showBadgesGlobalAuthority));
layout.append(
this->createCheckBox(("Show channel badges (broadcaster, moderator)"),
getSettings()->showBadgesChannelAuthority));
layout.append(this->createCheckBox(("Show subscriber badges "),
getSettings()->showBadgesSubscription));
layout.append(
this->createCheckBox(("Show vanity badges (prime, bits, subgifter)"),
getSettings()->showBadgesVanity));
layout.append(this->createCheckBox(("Show chatterino badges"),
getSettings()->showBadgesChatterino));
layout->addStretch(1);
}
void LookPage::addLastReadMessageIndicatorPatternSelector( void LookPage::addLastReadMessageIndicatorPatternSelector(
LayoutCreator<QVBoxLayout> layout) LayoutCreator<QVBoxLayout> layout)
{ {

View file

@ -23,6 +23,7 @@ private:
void addMessageTab(LayoutCreator<QVBoxLayout> layout); void addMessageTab(LayoutCreator<QVBoxLayout> layout);
void addEmoteTab(LayoutCreator<QVBoxLayout> layout); void addEmoteTab(LayoutCreator<QVBoxLayout> layout);
void addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout); void addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout);
void addBadgesTab(LayoutCreator<QVBoxLayout> layout);
void addLastReadMessageIndicatorPatternSelector( void addLastReadMessageIndicatorPatternSelector(
LayoutCreator<QVBoxLayout> layout); LayoutCreator<QVBoxLayout> layout);

View file

@ -149,7 +149,7 @@ ModerationPage::ModerationPage()
auto modMode = tabs.appendTab(new QVBoxLayout, "Moderation buttons"); auto modMode = tabs.appendTab(new QVBoxLayout, "Moderation buttons");
{ {
// clang-format off // clang-format off
auto label = modMode.emplace<QLabel>("Click the moderation mod button (<img width='18' height='18' src=':/images/moderatormode_disabled.png'>) in a channel that you moderate to enable moderator mode.<br>"); auto label = modMode.emplace<QLabel>("Click the moderation mod button (<img width='18' height='18' src=':/buttons/modModeDisabled.png'>) in a channel that you moderate to enable moderator mode.<br>");
label->setWordWrap(true); label->setWordWrap(true);
label->setStyleSheet("color: #bbb"); label->setStyleSheet("color: #bbb");
// clang-format on // clang-format on

View file

@ -1,6 +1,7 @@
#include "SettingsPage.hpp" #include "SettingsPage.hpp"
#include <QDebug> #include <QDebug>
#include <QPainter>
namespace chatterino { namespace chatterino {

View file

@ -363,6 +363,12 @@ void Split::handleModifiers(Qt::KeyboardModifiers modifiers)
} }
} }
void Split::setIsTopRightSplit(bool value)
{
this->isTopRightSplit_ = value;
this->header_->setAddButtonVisible(value);
}
/// Slots /// Slots
void Split::addSibling() void Split::addSibling()
{ {

View file

@ -65,6 +65,7 @@ public:
void layoutMessages(); void layoutMessages();
void updateGifEmotes(); void updateGifEmotes();
void updateLastReadMessage(); void updateLastReadMessage();
void setIsTopRightSplit(bool value);
void drag(); void drag();
@ -106,10 +107,11 @@ private:
NullablePtr<SelectChannelDialog> selectChannelDialog_; NullablePtr<SelectChannelDialog> selectChannelDialog_;
bool moderationMode_ = false; bool moderationMode_{};
bool isTopRightSplit_{};
bool isMouseOver_ = false; bool isMouseOver_{};
bool isDragging_ = false; bool isDragging_{};
pajlada::Signals::Connection channelIDChangedConnection_; pajlada::Signals::Connection channelIDChangedConnection_;
pajlada::Signals::Connection usermodeChangedConnection_; pajlada::Signals::Connection usermodeChangedConnection_;

View file

@ -310,8 +310,33 @@ void SplitContainer::focusSplitRecursive(Node *node, Direction direction)
} }
} }
Split *SplitContainer::getTopRightSplit(Node &node)
{
switch (node.getType()) {
case Node::_Split:
return node.getSplit();
case Node::VerticalContainer:
if (!node.getChildren().empty())
return getTopRightSplit(*node.getChildren().front());
break;
case Node::HorizontalContainer:
if (!node.getChildren().empty())
return getTopRightSplit(*node.getChildren().back());
break;
default:;
}
return nullptr;
}
void SplitContainer::layout() void SplitContainer::layout()
{ {
// update top right split
auto topRight = this->getTopRightSplit(this->baseNode_);
if (this->topRight_) this->topRight_->setIsTopRightSplit(false);
this->topRight_ = topRight;
if (topRight) this->topRight_->setIsTopRightSplit(true);
// layout
this->baseNode_.geometry_ = this->rect().adjusted(-1, -1, 0, 0); this->baseNode_.geometry_ = this->rect().adjusted(-1, -1, 0, 0);
std::vector<DropRect> _dropRects; std::vector<DropRect> _dropRects;
@ -431,9 +456,10 @@ void SplitContainer::paintEvent(QPaintEvent *)
if (notebook != nullptr) { if (notebook != nullptr) {
if (notebook->getPageCount() > 1) { if (notebook->getPageCount() > 1) {
text += "\n\nTip: After adding a split you can hold <Alt> to " text +=
"move it or split it " "\n\nTip: After adding a split you can hold <Ctrl+Alt> to "
"further."; "move it or split it "
"further.";
} }
} }

View file

@ -219,6 +219,7 @@ private:
void addSplit(Split *split); void addSplit(Split *split);
void decodeNodeRecusively(QJsonObject &obj, Node *node); void decodeNodeRecusively(QJsonObject &obj, Node *node);
Split *getTopRightSplit(Node &node);
struct DropRegion { struct DropRegion {
QRect rect; QRect rect;
@ -239,6 +240,7 @@ private:
Node baseNode_; Node baseNode_;
Split *selected_; Split *selected_;
Split *topRight_{};
NotebookTab *tab_; NotebookTab *tab_;
std::vector<Split *> splits_; std::vector<Split *> splits_;

View file

@ -93,11 +93,11 @@ namespace {
title += " (live)"; title += " (live)";
// description // description
if (settings.showUptime) title += " - " + s.uptime;
if (settings.showViewerCount) if (settings.showViewerCount)
title += " - " + QString::number(s.viewerCount); title += " - " + QString::number(s.viewerCount);
if (settings.showTitle) title += " - " + s.title;
if (settings.showGame) title += " - " + s.game; if (settings.showGame) title += " - " + s.game;
if (settings.showUptime) title += " - " + s.uptime; if (settings.showTitle) title += " - " + s.title;
return title; return title;
} }
@ -138,33 +138,42 @@ SplitHeader::SplitHeader(Split *_split)
void SplitHeader::initializeLayout() void SplitHeader::initializeLayout()
{ {
auto layout = makeLayout<QHBoxLayout>( auto layout = makeLayout<QHBoxLayout>({
{// title // title
this->titleLabel = makeWidget<Label>([](auto w) { this->titleLabel_ = makeWidget<Label>([](auto w) {
w->setSizePolicy(QSizePolicy::MinimumExpanding, w->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred); QSizePolicy::Preferred);
w->setCentered(true); w->setCentered(true);
w->setHasOffset(false); w->setHasOffset(false);
}), }),
// mode // mode
this->modeButton_ = makeWidget<EffectLabel>([&](auto w) { this->modeButton_ = makeWidget<EffectLabel>([&](auto w) {
w->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); w->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
w->hide(); w->hide();
this->initializeModeSignals(*w); this->initializeModeSignals(*w);
w->setMenu(this->createChatModeMenu()); w->setMenu(this->createChatModeMenu());
}), }),
// moderator // moderator
this->moderationButton_ = makeWidget<Button>([&](auto w) { this->moderationButton_ = makeWidget<Button>([&](auto w) {
QObject::connect(w, &Button::clicked, this, [this, w]() mutable { QObject::connect(w, &Button::clicked, this, [this, w]() mutable {
this->split_->setModerationMode( this->split_->setModerationMode(
!this->split_->getModerationMode()); !this->split_->getModerationMode());
w->setDim(!this->split_->getModerationMode()); w->setDim(!this->split_->getModerationMode());
}); });
}), }),
// dropdown // dropdown
this->dropdownButton_ = makeWidget<Button>( this->dropdownButton_ = makeWidget<Button>(
[&](auto w) { w->setMenu(this->createMainMenu()); })}); [&](auto w) { w->setMenu(this->createMainMenu()); }),
// add split
this->addButton_ = makeWidget<Button>([&](auto w) {
w->setPixmap(getApp()->resources->buttons.addSplitDark);
w->setEnableMargin(false);
QObject::connect(w, &Button::clicked, this,
[this]() { this->split_->addSibling(); });
}),
});
layout->setMargin(0); layout->setMargin(0);
layout->setSpacing(0); layout->setSpacing(0);
@ -174,18 +183,16 @@ void SplitHeader::initializeLayout()
std::unique_ptr<QMenu> SplitHeader::createMainMenu() std::unique_ptr<QMenu> SplitHeader::createMainMenu()
{ {
auto menu = std::make_unique<QMenu>(); auto menu = std::make_unique<QMenu>();
menu->addAction("New split", this->split_, &Split::addSibling, menu->addAction("Close channel", this->split_, &Split::deleteFromContainer,
QKeySequence("Ctrl+T"));
menu->addAction("Close split", this->split_, &Split::deleteFromContainer,
QKeySequence("Ctrl+W")); QKeySequence("Ctrl+W"));
menu->addAction("Change channel", this->split_, &Split::changeChannel, menu->addAction("Change channel", this->split_, &Split::changeChannel,
QKeySequence("Ctrl+R")); QKeySequence("Ctrl+R"));
menu->addSeparator(); menu->addSeparator();
menu->addAction("Popup", this->split_, &Split::popup);
menu->addAction("Viewer list", this->split_, &Split::showViewerList); menu->addAction("Viewer list", this->split_, &Split::showViewerList);
menu->addAction("Search", this->split_, &Split::showSearch, menu->addAction("Search", this->split_, &Split::showSearch,
QKeySequence("Ctrl+F")); QKeySequence("Ctrl+F"));
menu->addSeparator(); menu->addSeparator();
menu->addAction("Popup", this->split_, &Split::popup);
#ifdef USEWEBENGINE #ifdef USEWEBENGINE
this->dropdownMenu.addAction("Start watching", this, [this] { this->dropdownMenu.addAction("Start watching", this, [this] {
ChannelPtr _channel = this->split->getChannel(); ChannelPtr _channel = this->split->getChannel();
@ -224,7 +231,7 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
menu->addSeparator(); menu->addSeparator();
menu->addAction("Reload channel emotes", this, SLOT(reloadChannelEmotes())); menu->addAction("Reload channel emotes", this, SLOT(reloadChannelEmotes()));
menu->addAction("Reconnect", this, SLOT(reconnect())); menu->addAction("Reconnect", this, SLOT(reconnect()));
// menu->addAction("Clear messages", this->split_, &Split::doClearChat); menu->addAction("Clear messages", this->split_, &Split::clear);
// menu->addSeparator(); // menu->addSeparator();
// menu->addAction("Show changelog", this, SLOT(menuShowChangelog())); // menu->addAction("Show changelog", this, SLOT(menuShowChangelog()));
@ -351,6 +358,12 @@ void SplitHeader::scaleChangedEvent(float scale)
this->setFixedHeight(w); this->setFixedHeight(w);
this->dropdownButton_->setFixedWidth(w); this->dropdownButton_->setFixedWidth(w);
this->moderationButton_->setFixedWidth(w); this->moderationButton_->setFixedWidth(w);
this->addButton_->setFixedWidth(w * 5 / 8);
}
void SplitHeader::setAddButtonVisible(bool value)
{
this->addButton_->setVisible(value);
} }
void SplitHeader::updateChannelText() void SplitHeader::updateChannelText()
@ -375,7 +388,7 @@ void SplitHeader::updateChannelText()
} }
} }
this->titleLabel->setText(title.isEmpty() ? "<empty>" : title); this->titleLabel_->setText(title.isEmpty() ? "<empty>" : title);
} }
void SplitHeader::updateModerationModeIcon() void SplitHeader::updateModerationModeIcon()
@ -475,6 +488,8 @@ void SplitHeader::enterEvent(QEvent *event)
tooltip->moveTo(this, this->mapToGlobal(this->rect().bottomLeft()), tooltip->moveTo(this, this->mapToGlobal(this->rect().bottomLeft()),
false); false);
tooltip->setText(this->tooltipText_); tooltip->setText(this->tooltipText_);
tooltip->setWordWrap(false);
tooltip->adjustSize();
tooltip->show(); tooltip->show();
tooltip->raise(); tooltip->raise();
} }
@ -499,7 +514,7 @@ void SplitHeader::themeChangedEvent()
} else { } else {
palette.setColor(QPalette::Foreground, this->theme->splits.header.text); palette.setColor(QPalette::Foreground, this->theme->splits.header.text);
} }
this->titleLabel->setPalette(palette); this->titleLabel_->setPalette(palette);
// -- // --
if (this->theme->isLightTheme()) { if (this->theme->isLightTheme()) {

View file

@ -24,6 +24,8 @@ class SplitHeader final : public BaseWidget, pajlada::Signals::SignalHolder
public: public:
explicit SplitHeader(Split *_chatWidget); explicit SplitHeader(Split *_chatWidget);
void setAddButtonVisible(bool value);
void updateChannelText(); void updateChannelText();
void updateModerationModeIcon(); void updateModerationModeIcon();
void updateRoomModes(); void updateRoomModes();
@ -53,9 +55,10 @@ private:
// ui // ui
Button *dropdownButton_{}; Button *dropdownButton_{};
Label *titleLabel{}; Label *titleLabel_{};
EffectLabel *modeButton_{}; EffectLabel *modeButton_{};
Button *moderationButton_{}; Button *moderationButton_{};
Button *addButton_{};
// states // states
QPoint dragStart_{}; QPoint dragStart_{};

View file

@ -84,21 +84,8 @@ void SplitInput::initLayout()
})); }));
// open emote popup // open emote popup
QObject::connect(this->ui_.emoteButton, &EffectLabel::clicked, [this] { QObject::connect(this->ui_.emoteButton, &EffectLabel::clicked,
if (!this->emotePopup_) { [=] { this->openEmotePopup(); });
this->emotePopup_ = std::make_unique<EmotePopup>();
this->emotePopup_->linkClicked.connect([this](const Link &link) {
if (link.type == Link::InsertText) {
this->insertText(link.value + " ");
}
});
}
this->emotePopup_->resize(int(300 * this->emotePopup_->getScale()),
int(500 * this->emotePopup_->getScale()));
this->emotePopup_->loadChannel(this->split_->getChannel());
this->emotePopup_->show();
});
// clear channelview selection when selecting in the input // clear channelview selection when selecting in the input
QObject::connect(this->ui_.textEdit, &QTextEdit::copyAvailable, QObject::connect(this->ui_.textEdit, &QTextEdit::copyAvailable,
@ -159,6 +146,23 @@ void SplitInput::updateEmoteButton()
this->ui_.emoteButton->setFixedHeight(int(18 * scale)); this->ui_.emoteButton->setFixedHeight(int(18 * scale));
} }
void SplitInput::openEmotePopup()
{
if (!this->emotePopup_) {
this->emotePopup_ = std::make_unique<EmotePopup>();
this->emotePopup_->linkClicked.connect([this](const Link &link) {
if (link.type == Link::InsertText) {
this->insertText(link.value + " ");
}
});
}
this->emotePopup_->resize(int(300 * this->emotePopup_->getScale()),
int(500 * this->emotePopup_->getScale()));
this->emotePopup_->loadChannel(this->split_->getChannel());
this->emotePopup_->show();
}
void SplitInput::installKeyPressedEvent() void SplitInput::installKeyPressedEvent()
{ {
auto app = getApp(); auto app = getApp();
@ -177,7 +181,8 @@ void SplitInput::installKeyPressedEvent()
c->sendMessage(sendMessage); c->sendMessage(sendMessage);
// don't add duplicate messages and empty message to message history // don't add duplicate messages and empty message to message history
if ((this->prevMsg_.isEmpty() || !this->prevMsg_.endsWith(message)) && if ((this->prevMsg_.isEmpty() ||
!this->prevMsg_.endsWith(message)) &&
!message.trimmed().isEmpty()) !message.trimmed().isEmpty())
this->prevMsg_.append(message); this->prevMsg_.append(message);
@ -186,7 +191,7 @@ void SplitInput::installKeyPressedEvent()
this->currMsg_ = QString(); this->currMsg_ = QString();
this->ui_.textEdit->setText(QString()); this->ui_.textEdit->setText(QString());
this->prevIndex_ = 0; this->prevIndex_ = 0;
} else if (this->ui_.textEdit->toPlainText() == } else if (message ==
this->prevMsg_.at(this->prevMsg_.size() - 1)) { this->prevMsg_.at(this->prevMsg_.size() - 1)) {
this->prevMsg_.removeLast(); this->prevMsg_.removeLast();
} }
@ -227,6 +232,13 @@ void SplitInput::installKeyPressedEvent()
page->selectNextSplit(SplitContainer::Below); page->selectNextSplit(SplitContainer::Below);
} }
} else { } else {
// If user did not write anything before then just do nothing.
if (this->prevMsg_.isEmpty()) {
return;
}
bool cursorToEnd = true;
QString message = ui_.textEdit->toPlainText();
if (this->prevIndex_ != (this->prevMsg_.size() - 1) && if (this->prevIndex_ != (this->prevMsg_.size() - 1) &&
this->prevIndex_ != this->prevMsg_.size()) { this->prevIndex_ != this->prevMsg_.size()) {
this->prevIndex_++; this->prevIndex_++;
@ -234,12 +246,27 @@ void SplitInput::installKeyPressedEvent()
this->prevMsg_.at(this->prevIndex_)); this->prevMsg_.at(this->prevIndex_));
} else { } else {
this->prevIndex_ = this->prevMsg_.size(); this->prevIndex_ = this->prevMsg_.size();
this->ui_.textEdit->setText(this->currMsg_); if (message == this->prevMsg_.at(this->prevIndex_ - 1)) {
// If user has just come from a message history
// Then simply get currMsg_.
this->ui_.textEdit->setText(this->currMsg_);
} else if (message != this->currMsg_) {
// If user are already in current message
// And type something new
// Then replace currMsg_ with new one.
this->currMsg_ = message;
}
// If user is already in current message
// Then don't touch cursos.
cursorToEnd =
(message == this->prevMsg_.at(this->prevIndex_ - 1));
} }
QTextCursor cursor = this->ui_.textEdit->textCursor(); if (cursorToEnd) {
cursor.movePosition(QTextCursor::End); QTextCursor cursor = this->ui_.textEdit->textCursor();
this->ui_.textEdit->setTextCursor(cursor); cursor.movePosition(QTextCursor::End);
this->ui_.textEdit->setTextCursor(cursor);
}
} }
} else if (event->key() == Qt::Key_Left) { } else if (event->key() == Qt::Key_Left) {
if (event->modifiers() == Qt::AltModifier) { if (event->modifiers() == Qt::AltModifier) {
@ -284,6 +311,9 @@ void SplitInput::installKeyPressedEvent()
this->split_->copyToClipboard(); this->split_->copyToClipboard();
event->accept(); event->accept();
} }
} else if (event->key() == Qt::Key_E &&
event->modifiers() == Qt::ControlModifier) {
this->openEmotePopup();
} }
}); });
} }

View file

@ -43,6 +43,7 @@ private:
void initLayout(); void initLayout();
void installKeyPressedEvent(); void installKeyPressedEvent();
void updateEmoteButton(); void updateEmoteButton();
void openEmotePopup();
Split *const split_; Split *const split_;
std::shared_ptr<EmotePopup> emotePopup_; std::shared_ptr<EmotePopup> emotePopup_;