diff --git a/.clang-format b/.clang-format index bd3f2c430..eeb9eb929 100644 --- a/.clang-format +++ b/.clang-format @@ -25,6 +25,9 @@ DerivePointerBinding: false FixNamespaceComments: true IndentCaseLabels: true IndentWidth: 4 +IndentWrappedFunctionNames: true +IndentPPDirectives: AfterHash +NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 Standard: Auto diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index aaa8743c3..38b48e033 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -10,11 +10,11 @@ ### OpenSSL #### For our websocket library, we need OpenSSL 1.1 -1. Download OpenSSL development library: https://slproweb.com/download/Win64OpenSSL-1_1_0h.exe +1. Download OpenSSL development library: https://slproweb.com/download/Win64OpenSSL-1_1_0i.exe 2. When prompted, install openssl to C:\local\openssl 3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory" #### For Qt SSL, we need OpenSSL 1.0 -1. Download OpenSSL light: https://slproweb.com/download/Win64OpenSSL_Light-1_0_2o.exe +1. Download OpenSSL light: https://slproweb.com/download/Win64OpenSSL_Light-1_0_2p.exe 2. When prompted, install it anywhere 3. When prompted, copy the OpenSSL DLLS to "The OpenSSL binaries (/bin) directory" 4. Copy the OpenSSL 1.0 files from its /bin folder to C:/local/bin (You will need to create the folder) diff --git a/Jenkinsfile b/Jenkinsfile index 16780a908..621ef91eb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,12 +6,12 @@ pipeline { parallel { stage('GCC') { steps { - sh 'mkdir -p build-linux-gcc && cd build-linux-gcc && qmake .. && make' + sh 'mkdir -p build-linux-gcc && cd build-linux-gcc && make distclean; qmake .. && make' } } stage('Clang') { steps { - sh 'mkdir -p build-linux-clang && cd build-linux-clang && qmake -spec linux-clang .. && make' + sh 'mkdir -p build-linux-clang && cd build-linux-clang && make distclean; qmake -spec linux-clang .. && make' } } } diff --git a/chatterino.pro b/chatterino.pro index 8003eba1a..16ba15c6b 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -6,7 +6,7 @@ message(----) -QT += widgets core gui network multimedia svg +QT += widgets core gui network multimedia svg concurrent CONFIG += communi COMMUNI += core model util CONFIG += c++14 @@ -33,7 +33,7 @@ equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { } # Icons -macx:ICON = resources/images/chatterino2.icns +#macx:ICON = resources/images/chatterino2.icns win32:RC_FILE = resources/windows.rc @@ -72,6 +72,8 @@ win32 { # OSX include directory macx { INCLUDEPATH += /usr/local/include + INCLUDEPATH += /usr/local/opt/openssl/include + LIBS += -L/usr/local/opt/openssl/lib } # Optional dependency on Windows SDK 7 @@ -106,7 +108,6 @@ SOURCES += \ src/Application.cpp \ src/common/Channel.cpp \ src/common/CompletionModel.cpp \ - src/common/Emotemap.cpp \ src/common/NetworkData.cpp \ src/common/NetworkManager.cpp \ src/common/NetworkRequest.cpp \ @@ -188,8 +189,6 @@ SOURCES += \ src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/ResizingTextEdit.cpp \ - src/widgets/helper/RippleEffectButton.cpp \ - src/widgets/helper/RippleEffectLabel.cpp \ src/widgets/helper/ScrollbarHighlight.cpp \ src/widgets/helper/SearchPopup.cpp \ src/widgets/helper/SettingsDialogTab.cpp \ @@ -239,7 +238,6 @@ SOURCES += \ src/providers/twitch/PubsubClient.cpp \ src/providers/twitch/TwitchApi.cpp \ src/messages/Emote.cpp \ - src/messages/EmoteMap.cpp \ src/messages/ImageSet.cpp \ src/providers/bttv/BttvEmotes.cpp \ src/providers/ffz/FfzEmotes.cpp \ @@ -257,16 +255,20 @@ SOURCES += \ src/controllers/notifications/NotificationModel.cpp \ src/singletons/Toasts.cpp \ src/common/DownloadManager.cpp + src/widgets/helper/EffectLabel.cpp \ + src/widgets/helper/Button.cpp \ + src/messages/MessageContainer.cpp \ + src/debug/Benchmark.cpp \ + src/common/UsernameSet.cpp \ + src/widgets/settingspages/AdvancedPage.cpp HEADERS += \ src/Application.hpp \ src/common/Channel.hpp \ src/common/Common.hpp \ src/common/CompletionModel.hpp \ - src/common/Emotemap.hpp \ src/common/FlagsEnum.hpp \ - src/common/LockedObject.hpp \ - src/common/MutexValue.hpp \ + src/common/Atomic.hpp \ src/common/NetworkCommon.hpp \ src/common/NetworkData.hpp \ src/common/NetworkManager.hpp \ @@ -276,9 +278,8 @@ HEADERS += \ src/common/NetworkTimer.hpp \ src/common/NetworkWorker.hpp \ src/common/NullablePtr.hpp \ - src/common/Property.hpp \ src/common/ProviderId.hpp \ - src/common/SerializeCustom.hpp \ + src/util/RapidJsonSerializeQString.hpp \ src/common/SignalVectorModel.hpp \ src/common/Version.hpp \ src/controllers/accounts/Account.hpp \ @@ -314,7 +315,6 @@ HEADERS += \ src/messages/MessageBuilder.hpp \ src/messages/MessageColor.hpp \ src/messages/MessageElement.hpp \ - src/messages/MessageParseArgs.hpp \ src/messages/Selection.hpp \ src/PrecompiledHeader.hpp \ src/providers/emoji/Emojis.hpp \ @@ -381,8 +381,6 @@ HEADERS += \ src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookTab.hpp \ src/widgets/helper/ResizingTextEdit.hpp \ - src/widgets/helper/RippleEffectButton.hpp \ - src/widgets/helper/RippleEffectLabel.hpp \ src/widgets/helper/ScrollbarHighlight.hpp \ src/widgets/helper/SearchPopup.hpp \ src/widgets/helper/SettingsDialogTab.hpp \ @@ -426,7 +424,6 @@ HEADERS += \ src/singletons/Updates.hpp \ src/singletons/NativeMessaging.hpp \ src/singletons/Theme.hpp \ - src/common/SimpleSignalVector.hpp \ src/common/SignalVector.hpp \ src/widgets/dialogs/LogsPopup.hpp \ src/common/Singleton.hpp \ @@ -439,8 +436,6 @@ HEADERS += \ src/providers/twitch/PubsubClient.hpp \ src/providers/twitch/TwitchApi.hpp \ src/messages/Emote.hpp \ - src/messages/EmoteMap.hpp \ - src/messages/EmoteCache.hpp \ src/messages/ImageSet.hpp \ src/common/Outcome.hpp \ src/providers/bttv/BttvEmotes.hpp \ @@ -460,6 +455,12 @@ HEADERS += \ src/controllers/notifications/NotificationModel.hpp \ src/singletons/Toasts.hpp \ src/common/DownloadManager.hpp + src/widgets/helper/EffectLabel.hpp \ + src/util/LayoutHelper.hpp \ + src/widgets/helper/Button.hpp \ + src/messages/MessageContainer.hpp \ + src/common/UsernameSet.hpp \ + src/widgets/settingspages/AdvancedPage.hpp RESOURCES += \ resources/resources.qrc \ diff --git a/resources/licenses/emoji-data-source.txt b/resources/licenses/emoji-data-source.txt new file mode 100644 index 000000000..85ddb3c0f --- /dev/null +++ b/resources/licenses/emoji-data-source.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Cal Henderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/settings/emote.svg b/resources/settings/emote.svg new file mode 100644 index 000000000..10e25c9f5 --- /dev/null +++ b/resources/settings/emote.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/Application.cpp b/src/Application.cpp index cde5c1c09..bce4cd504 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -7,11 +7,13 @@ #include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/notifications/NotificationController.hpp" #include "controllers/taggedusers/TaggedUsersController.hpp" +#include "debug/Log.hpp" #include "messages/MessageBuilder.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchServer.hpp" +#include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" #include "singletons/Logging.hpp" #include "singletons/NativeMessaging.hpp" @@ -23,6 +25,7 @@ #include "singletons/WindowManager.hpp" #include "util/IsBigEndian.hpp" #include "util/PostToThread.hpp" +#include "widgets/Window.hpp" #include @@ -37,9 +40,7 @@ Application *Application::instance = nullptr; // to each other Application::Application(Settings &_settings, Paths &_paths) - : settings(&_settings) - , paths(&_paths) - , resources(&this->emplace()) + : resources(&this->emplace()) , themes(&this->emplace()) , fonts(&this->emplace()) @@ -103,26 +104,26 @@ void Application::save() void Application::initNm() { #ifdef Q_OS_WIN -#ifdef QT_DEBUG -#ifdef C_DEBUG_NM +# ifdef QT_DEBUG +# ifdef C_DEBUG_NM this->nativeMessaging->registerHost(); this->nativeMessaging->openGuiMessageQueue(); -#endif -#else +# endif +# else this->nativeMessaging->registerHost(); this->nativeMessaging->openGuiMessageQueue(); -#endif +# endif #endif } void Application::initPubsub() { this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) { - Log("WHISPER SENT LOL"); // + log("WHISPER SENT LOL"); // }); this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) { - Log("WHISPER RECEIVED LOL"); // + log("WHISPER RECEIVED LOL"); // }); this->twitch.pubsub->signals_.moderation.chatCleared.connect( diff --git a/src/Application.hpp b/src/Application.hpp index b1dbe2406..b7df63d9d 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,7 +1,6 @@ #pragma once #include "common/Singleton.hpp" -#include "singletons/Resources.hpp" #include #include @@ -27,9 +26,10 @@ class AccountManager; class Emotes; class Settings; class Fonts; -class Resources; +class Resources2; class Toasts; + class Application { std::vector> singletons_; @@ -49,8 +49,6 @@ public: friend void test(); - Settings *const settings{}; - Paths *const paths{}; Resources2 *const resources; Theme *const themes{}; diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index 75a6ef26f..f12d19b5e 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -9,33 +9,31 @@ #include #ifdef Q_OS_WIN -#include -#include -#include +# include +# include +# include #endif namespace chatterino { namespace { -void initFileMode() -{ + void initFileMode() + { #ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); #endif -} + } -void runLoop(NativeMessagingClient &client) -{ - while (true) { - char size_c[4]; - std::cin.read(size_c, 4); + void runLoop(NativeMessagingClient &client) + { + while (true) { + char size_c[4]; + std::cin.read(size_c, 4); - if (std::cin.eof()) { - break; - } + if (std::cin.eof()) break; - uint32_t size = *reinterpret_cast(size_c); + auto size = *reinterpret_cast(size_c); #if 0 bool bigEndian = isBigEndian(); @@ -50,14 +48,14 @@ void runLoop(NativeMessagingClient &client) } #endif - std::unique_ptr b(new char[size + 1]); - std::cin.read(b.get(), size); - *(b.get() + size) = '\0'; + std::unique_ptr buffer(new char[size + 1]); + std::cin.read(buffer.get(), size); + *(buffer.get() + size) = '\0'; - client.sendMessage( - QByteArray::fromRawData(b.get(), static_cast(size))); + client.sendMessage(QByteArray::fromRawData( + buffer.get(), static_cast(size))); + } } -} } // namespace bool shouldRunBrowserExtensionHost(const QStringList &args) diff --git a/src/PrecompiledHeader.hpp b/src/PrecompiledHeader.hpp index 6eec78c8e..10dfcd418 100644 --- a/src/PrecompiledHeader.hpp +++ b/src/PrecompiledHeader.hpp @@ -1,168 +1,168 @@ #ifdef __cplusplus -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include -#ifndef UNUSED -#define UNUSED(x) (void)(x) -#endif +# ifndef UNUSED +# define UNUSED(x) (void)(x) +# endif -#ifndef ATTR_UNUSED -#ifdef Q_OS_WIN -#define ATTR_UNUSED -#else -#define ATTR_UNUSED __attribute__((unused)) -#endif -#endif +# ifndef ATTR_UNUSED +# ifdef Q_OS_WIN +# define ATTR_UNUSED +# else +# define ATTR_UNUSED __attribute__((unused)) +# endif +# endif #endif diff --git a/src/RunGui.cpp b/src/RunGui.cpp index d6c4b0306..e342ca7c1 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -12,7 +12,7 @@ #include "widgets/dialogs/LastRunCrashDialog.hpp" #ifdef C_USE_BREAKPAD -#include +# include #endif // void initQt(); @@ -23,82 +23,82 @@ namespace chatterino { namespace { -void installCustomPalette() -{ - // borrowed from - // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows - QPalette darkPalette = qApp->palette(); + void installCustomPalette() + { + // borrowed from + // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows + auto dark = qApp->palette(); - darkPalette.setColor(QPalette::Window, QColor(22, 22, 22)); - darkPalette.setColor(QPalette::WindowText, Qt::white); - darkPalette.setColor(QPalette::Text, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, - QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Base, QColor("#333")); - darkPalette.setColor(QPalette::AlternateBase, QColor("#444")); - darkPalette.setColor(QPalette::ToolTipBase, Qt::white); - darkPalette.setColor(QPalette::ToolTipText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::Text, - QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Dark, QColor(35, 35, 35)); - darkPalette.setColor(QPalette::Shadow, QColor(20, 20, 20)); - darkPalette.setColor(QPalette::Button, QColor(70, 70, 70)); - darkPalette.setColor(QPalette::ButtonText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, - QColor(127, 127, 127)); - darkPalette.setColor(QPalette::BrightText, Qt::red); - darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Disabled, QPalette::Highlight, - QColor(80, 80, 80)); - darkPalette.setColor(QPalette::HighlightedText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, - QColor(127, 127, 127)); + dark.setColor(QPalette::Window, QColor(22, 22, 22)); + dark.setColor(QPalette::WindowText, Qt::white); + dark.setColor(QPalette::Text, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::WindowText, + QColor(127, 127, 127)); + dark.setColor(QPalette::Base, QColor("#333")); + dark.setColor(QPalette::AlternateBase, QColor("#444")); + dark.setColor(QPalette::ToolTipBase, Qt::white); + dark.setColor(QPalette::ToolTipText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::Text, + QColor(127, 127, 127)); + dark.setColor(QPalette::Dark, QColor(35, 35, 35)); + dark.setColor(QPalette::Shadow, QColor(20, 20, 20)); + dark.setColor(QPalette::Button, QColor(70, 70, 70)); + dark.setColor(QPalette::ButtonText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::ButtonText, + QColor(127, 127, 127)); + dark.setColor(QPalette::BrightText, Qt::red); + dark.setColor(QPalette::Link, QColor(42, 130, 218)); + dark.setColor(QPalette::Highlight, QColor(42, 130, 218)); + dark.setColor(QPalette::Disabled, QPalette::Highlight, + QColor(80, 80, 80)); + dark.setColor(QPalette::HighlightedText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::HighlightedText, + QColor(127, 127, 127)); - qApp->setPalette(darkPalette); -} - -void initQt() -{ - // set up the QApplication flags - QApplication::setAttribute(Qt::AA_Use96Dpi, true); -#ifdef Q_OS_WIN32 - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - - QApplication::setStyle(QStyleFactory::create("Fusion")); - - installCustomPalette(); -} - -void showLastCrashDialog() -{ -#ifndef C_DISABLE_CRASH_DIALOG - LastRunCrashDialog dialog; - - switch (dialog.exec()) { - case QDialog::Accepted: { - }; break; - default: { - _exit(0); - } + qApp->setPalette(dark); } + + void initQt() + { + // set up the QApplication flags + QApplication::setAttribute(Qt::AA_Use96Dpi, true); +#ifdef Q_OS_WIN32 + QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); #endif -} -void createRunningFile(const QString &path) -{ - QFile runningFile(path); + QApplication::setStyle(QStyleFactory::create("Fusion")); - runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); - runningFile.flush(); - runningFile.close(); -} + installCustomPalette(); + } -void removeRunningFile(const QString &path) -{ - QFile::remove(path); -} + void showLastCrashDialog() + { +#ifndef C_DISABLE_CRASH_DIALOG + LastRunCrashDialog dialog; + + switch (dialog.exec()) { + case QDialog::Accepted: { + }; break; + default: { + _exit(0); + } + } +#endif + } + + void createRunningFile(const QString &path) + { + QFile runningFile(path); + + runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); + runningFile.flush(); + runningFile.close(); + } + + void removeRunningFile(const QString &path) + { + QFile::remove(path); + } } // namespace void runGui(QApplication &a, Paths &paths, Settings &settings) @@ -109,7 +109,7 @@ void runGui(QApplication &a, Paths &paths, Settings &settings) chatterino::Updates::getInstance().checkForUpdates(); #ifdef C_USE_BREAKPAD - QBreakpadInstance.setDumpPath(app->paths->settingsFolderPath + "/Crashes"); + QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes"); #endif // Running file diff --git a/src/common/Aliases.hpp b/src/common/Aliases.hpp index 8a61a9639..2dea876b7 100644 --- a/src/common/Aliases.hpp +++ b/src/common/Aliases.hpp @@ -32,3 +32,5 @@ QStringAlias(UserName); QStringAlias(UserId); QStringAlias(Url); QStringAlias(Tooltip); +QStringAlias(EmoteId); +QStringAlias(EmoteName); diff --git a/src/common/MutexValue.hpp b/src/common/Atomic.hpp similarity index 66% rename from src/common/MutexValue.hpp rename to src/common/Atomic.hpp index 112257370..39d8119f6 100644 --- a/src/common/MutexValue.hpp +++ b/src/common/Atomic.hpp @@ -6,14 +6,14 @@ namespace chatterino { template -class MutexValue : boost::noncopyable +class Atomic : boost::noncopyable { public: - MutexValue() + Atomic() { } - MutexValue(T &&val) + Atomic(T &&val) : value_(val) { } @@ -32,6 +32,13 @@ public: this->value_ = val; } + void set(T &&val) + { + std::lock_guard guard(this->mutex_); + + this->value_ = std::move(val); + } + private: mutable std::mutex mutex_; T value_; diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 6511d6a2e..f9ac3ac50 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -18,16 +18,14 @@ namespace chatterino { +// +// Channel +// Channel::Channel(const QString &name, Type type) - : completionModel(name) + : completionModel(*this) , name_(name) , type_(type) { - QObject::connect(&this->clearCompletionModelTimer_, &QTimer::timeout, - [this]() { - this->completionModel.clearExpiredStrings(); // - }); - this->clearCompletionModelTimer_.start(60 * 1000); } Channel::~Channel() @@ -67,8 +65,7 @@ void Channel::addMessage(MessagePtr message) const QString &username = message->loginName; if (!username.isEmpty()) { - // TODO: Add recent chatters display name. This should maybe be a - // setting + // TODO: Add recent chatters display name this->addRecentChatter(message); } @@ -98,8 +95,6 @@ void Channel::addOrReplaceTimeout(MessagePtr message) for (int i = snapshotLength - 1; i >= end; --i) { auto &s = snapshot[i]; - qDebug() << s->parseTime << minimumTime; - if (s->parseTime < minimumTime) { break; } @@ -165,13 +160,14 @@ void Channel::disableAllMessages() LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); int snapshotLength = snapshot.getLength(); for (int i = 0; i < snapshotLength; i++) { - auto &s = snapshot[i]; - if (s->flags.hasAny({MessageFlag::System, MessageFlag::Timeout})) { + auto &message = snapshot[i]; + if (message->flags.hasAny( + {MessageFlag::System, MessageFlag::Timeout})) { continue; } // FOURTF: disabled for now - // s->flags.EnableFlag(MessageFlag::Disabled); + const_cast(message.get())->flags.set(MessageFlag::Disabled); } } @@ -196,7 +192,6 @@ void Channel::replaceMessage(MessagePtr message, MessagePtr replacement) void Channel::addRecentChatter(const MessagePtr &message) { - // Do nothing by default } bool Channel::canSendMessage() const @@ -234,9 +229,45 @@ void Channel::onConnected() { } -std::weak_ptr Channel::weak_from_this() +// +// Indirect channel +// +IndirectChannel::Data::Data(ChannelPtr _channel, Channel::Type _type) + : channel(_channel) + , type(_type) { - return std::weak_ptr(this->shared_from_this()); +} + +IndirectChannel::IndirectChannel(ChannelPtr channel, Channel::Type type) + : data_(std::make_unique(channel, type)) +{ +} + +ChannelPtr IndirectChannel::get() +{ + return data_->channel; +} + +void IndirectChannel::reset(ChannelPtr channel) +{ + assert(this->data_->type != Channel::Type::Direct); + + this->data_->channel = channel; + this->data_->changed.invoke(); +} + +pajlada::Signals::NoArgSignal &IndirectChannel::getChannelChanged() +{ + return this->data_->changed; +} + +Channel::Type IndirectChannel::getType() +{ + if (this->data_->type == Channel::Type::Direct) { + return this->get()->getType(); + } else { + return this->data_->type; + } } } // namespace chatterino diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 429123c5a..f265f4b70 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -1,10 +1,7 @@ #pragma once #include "common/CompletionModel.hpp" -#include "messages/Image.hpp" #include "messages/LimitedQueue.hpp" -#include "messages/Message.hpp" -#include "util/ConcurrentMap.hpp" #include #include @@ -13,7 +10,9 @@ #include namespace chatterino { + struct Message; +using MessagePtr = std::shared_ptr; class Channel : public std::enable_shared_from_this { @@ -52,7 +51,6 @@ public: void addOrReplaceTimeout(MessagePtr message); void disableAllMessages(); void replaceMessage(MessagePtr message, MessagePtr replacement); - virtual void addRecentChatter(const MessagePtr &message); QStringList modList; @@ -66,11 +64,9 @@ public: CompletionModel completionModel; - // pre c++17 polyfill - std::weak_ptr weak_from_this(); - protected: virtual void onConnected(); + virtual void addRecentChatter(const MessagePtr &message); private: const QString name_; @@ -88,47 +84,17 @@ class IndirectChannel Channel::Type type; pajlada::Signals::NoArgSignal changed; - Data() = delete; - Data(ChannelPtr _channel, Channel::Type _type) - : channel(_channel) - , type(_type) - { - } + Data(ChannelPtr channel, Channel::Type type); }; public: IndirectChannel(ChannelPtr channel, - Channel::Type type = Channel::Type::Direct) - : data_(new Data(channel, type)) - { - } + Channel::Type type = Channel::Type::Direct); - ChannelPtr get() - { - return data_->channel; - } - - void update(ChannelPtr ptr) - { - assert(this->data_->type != Channel::Type::Direct); - - this->data_->channel = ptr; - this->data_->changed.invoke(); - } - - pajlada::Signals::NoArgSignal &getChannelChanged() - { - return this->data_->changed; - } - - Channel::Type getType() - { - if (this->data_->type == Channel::Type::Direct) { - return this->get()->getType(); - } else { - return this->data_->type; - } - } + ChannelPtr get(); + void reset(ChannelPtr channel); + pajlada::Signals::NoArgSignal &getChannelChanged(); + Channel::Type getType(); private: std::shared_ptr data_; diff --git a/src/common/Common.hpp b/src/common/Common.hpp index 180dfb6a3..bea206f78 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -3,7 +3,6 @@ #include "common/Aliases.hpp" #include "common/Outcome.hpp" #include "common/ProviderId.hpp" -#include "debug/Log.hpp" #include #include @@ -40,4 +39,12 @@ std::weak_ptr weakOf(T *element) return element->shared_from_this(); } +struct Message; +using MessagePtr = std::shared_ptr; + +enum class CopyMode { + Everything, + OnlyTextAndEmotes, +}; + } // namespace chatterino diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index bd0038107..8ccafb8e5 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -2,45 +2,30 @@ #include "Application.hpp" #include "common/Common.hpp" +#include "common/UsernameSet.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" +#include "debug/Benchmark.hpp" #include "debug/Log.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Emotes.hpp" #include - #include namespace chatterino { -// -- TaggedString +// +// TaggedString +// -CompletionModel::TaggedString::TaggedString(const QString &_str, Type _type) - : str(_str) +CompletionModel::TaggedString::TaggedString(const QString &_string, Type _type) + : string(_string) , type(_type) - , timeAdded(std::chrono::steady_clock::now()) { } -bool CompletionModel::TaggedString::isExpired( - const std::chrono::steady_clock::time_point &now) const -{ - switch (this->type) { - case Type::Username: { - static std::chrono::minutes expirationTimer(10); - - return (this->timeAdded + expirationTimer < now); - } break; - - default: { - return false; - } break; - } - - return false; -} - bool CompletionModel::TaggedString::isEmote() const { return this->type > Type::EmoteStart && this->type < Type::EmoteEnd; @@ -48,35 +33,23 @@ bool CompletionModel::TaggedString::isEmote() const bool CompletionModel::TaggedString::operator<(const TaggedString &that) const { - if (this->isEmote()) { - if (that.isEmote()) { - int k = QString::compare(this->str, that.str, Qt::CaseInsensitive); - if (k == 0) { - return this->str > that.str; - } - - return k < 0; - } - - return true; + if (this->isEmote() != that.isEmote()) { + return this->isEmote(); } - if (that.isEmote()) { - return false; - } - - int k = QString::compare(this->str, that.str, Qt::CaseInsensitive); - if (k == 0) { - return false; - } + // try comparing insensitively, if they are the same then senstively + // (fixes order of LuL and LUL) + int k = QString::compare(this->string, that.string, Qt::CaseInsensitive); + if (k == 0) return this->string > that.string; return k < 0; } -// -- CompletionModel - -CompletionModel::CompletionModel(const QString &channelName) - : channelName_(channelName) +// +// CompletionModel +// +CompletionModel::CompletionModel(Channel &channel) + : channel_(channel) { } @@ -87,160 +60,90 @@ int CompletionModel::columnCount(const QModelIndex &) const QVariant CompletionModel::data(const QModelIndex &index, int) const { - std::lock_guard lock(this->emotesMutex_); + std::lock_guard lock(this->itemsMutex_); - // TODO: Implement more safely - auto it = this->emotes_.begin(); + auto it = this->items_.begin(); std::advance(it, index.row()); - return QVariant(it->str); + return QVariant(it->string); } int CompletionModel::rowCount(const QModelIndex &) const { - std::lock_guard lock(this->emotesMutex_); + std::lock_guard lock(this->itemsMutex_); - return this->emotes_.size(); + return this->items_.size(); } -void CompletionModel::refresh() +void CompletionModel::refresh(const QString &prefix) { - Log("[CompletionModel:{}] Refreshing...]", this->channelName_); + std::lock_guard guard(this->itemsMutex_); + this->items_.clear(); - auto app = getApp(); + if (prefix.length() < 2) return; - // User-specific: Twitch Emotes - if (auto account = app->accounts->twitch.getCurrent()) { - for (const auto &emote : account->accessEmotes()->allEmoteNames) { - // XXX: No way to discern between a twitch global emote and sub - // emote right now - this->addString(emote.string, - TaggedString::Type::TwitchGlobalEmote); - } - } - - // // Global: BTTV Global Emotes - // std::vector &bttvGlobalEmoteCodes = - // app->emotes->bttv.globalEmoteNames_; for (const auto &m : - // bttvGlobalEmoteCodes) { - // this->addString(m, TaggedString::Type::BTTVGlobalEmote); - // } - - // // Global: FFZ Global Emotes - // std::vector &ffzGlobalEmoteCodes = - // app->emotes->ffz.globalEmoteCodes; for (const auto &m : - // ffzGlobalEmoteCodes) { - // this->addString(m, TaggedString::Type::FFZGlobalEmote); - // } - - // Channel emotes - if (auto channel = dynamic_cast( - getApp() - ->twitch2->getChannelOrEmptyByID(this->channelName_) - .get())) { - auto bttv = channel->accessBttvEmotes(); - // auto it = bttv->begin(); - // for (const auto &emote : *bttv) { - // } - // std::vector &bttvChannelEmoteCodes = - // app->emotes->bttv.channelEmoteName_[this->channelName_]; - // for (const auto &m : bttvChannelEmoteCodes) { - // this->addString(m, TaggedString::Type::BTTVChannelEmote); - // } - - // Channel-specific: FFZ Channel Emotes - for (const auto &emote : *channel->accessFfzEmotes()) { - this->addString(emote.second->name.string, - TaggedString::Type::FFZChannelEmote); - } - } - - // Emojis - const auto &emojiShortCodes = app->emotes->emojis.shortCodes; - for (const auto &m : emojiShortCodes) { - this->addString(":" + m + ":", TaggedString::Type::Emoji); - } - - // Commands - for (auto &command : app->commands->items.getVector()) { - this->addString(command.name, TaggedString::Command); - } - - for (auto &command : app->commands->getDefaultTwitchCommandList()) { - this->addString(command, TaggedString::Command); - } - - // Channel-specific: Usernames - // fourtf: only works with twitch chat - // auto c = - // ChannelManager::getInstance().getTwitchChannel(this->channelName); - // auto usernames = c->getUsernamesForCompletions(); - // for (const auto &name : usernames) { - // assert(!name.displayName.isEmpty()); - // this->addString(name.displayName); - // this->addString('@' + name.displayName); - - // if (!name.localizedName.isEmpty()) { - // this->addString(name.localizedName); - // this->addString('@' + name.localizedName); - // } - // } -} - -void CompletionModel::addString(const QString &str, TaggedString::Type type) -{ - std::lock_guard lock(this->emotesMutex_); - - // Always add a space at the end of completions - this->emotes_.insert({str + " ", type}); -} - -void CompletionModel::addUser(const QString &username) -{ - auto add = [this](const QString &str) { - auto ts = this->createUser(str + " "); - // Always add a space at the end of completions - std::pair::iterator, bool> p = - this->emotes_.insert(ts); - if (!p.second) { - // No inseration was made, figure out if we need to replace the - // username. - - if (p.first->str > ts.str) { - // Replace lowercase version of name with mixed-case version - this->emotes_.erase(p.first); - auto result2 = this->emotes_.insert(ts); - assert(result2.second); - } else { - p.first->timeAdded = std::chrono::steady_clock::now(); - } - } + auto addString = [&](const QString &str, TaggedString::Type type) { + if (str.startsWith(prefix, Qt::CaseInsensitive)) + this->items_.emplace(str + " ", type); }; - add(username); - add("@" + username); -} + if (auto channel = dynamic_cast(&this->channel_)) { + // account emotes + if (auto account = getApp()->accounts->twitch.getCurrent()) { + for (const auto &emote : account->accessEmotes()->allEmoteNames) { + // XXX: No way to discern between a twitch global emote and sub + // emote right now + addString(emote.string, TaggedString::Type::TwitchGlobalEmote); + } + } -void CompletionModel::clearExpiredStrings() -{ - std::lock_guard lock(this->emotesMutex_); + // Usernames + if (prefix.length() >= UsernameSet::PrefixLength) { + auto usernames = channel->accessChatters(); - auto now = std::chrono::steady_clock::now(); + for (const auto &name : usernames->subrange(Prefix(prefix))) { + addString(name, TaggedString::Type::Username); + addString("@" + name, TaggedString::Type::Username); + } + } - for (auto it = this->emotes_.begin(); it != this->emotes_.end();) { - const auto &taggedString = *it; + // Bttv Global + for (auto &emote : *channel->globalBttv().emotes()) { + addString(emote.first.string, TaggedString::Type::BTTVChannelEmote); + } - if (taggedString.isExpired(now)) { - // Log("String {} expired", taggedString.str); - it = this->emotes_.erase(it); - } else { - ++it; + // Ffz Global + for (auto &emote : *channel->globalFfz().emotes()) { + addString(emote.first.string, TaggedString::Type::FFZChannelEmote); + } + + // Bttv Channel + for (auto &emote : *channel->bttvEmotes()) { + addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); + } + + // Ffz Channel + for (auto &emote : *channel->ffzEmotes()) { + addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); + } + + // Emojis + if (prefix.startsWith(":")) { + const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes; + for (auto &m : emojiShortCodes) { + addString(":" + m + ":", TaggedString::Type::Emoji); + } + } + + // Commands + for (auto &command : getApp()->commands->items.getVector()) { + addString(command.name, TaggedString::Command); + } + + for (auto &command : + getApp()->commands->getDefaultTwitchCommandList()) { + addString(command, TaggedString::Command); } } } -CompletionModel::TaggedString CompletionModel::createUser(const QString &str) -{ - return TaggedString{str, TaggedString::Type::Username}; -} - } // namespace chatterino diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index d9baac7b3..f4f216cd1 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -1,7 +1,5 @@ #pragma once -#include "common/Common.hpp" - #include #include @@ -10,7 +8,7 @@ namespace chatterino { -class TwitchChannel; +class Channel; class CompletionModel : public QAbstractListModel { @@ -33,39 +31,30 @@ class CompletionModel : public QAbstractListModel Command, }; - TaggedString(const QString &_str, Type _type); + TaggedString(const QString &string, Type type); - bool isExpired(const std::chrono::steady_clock::time_point &now) const; bool isEmote() const; bool operator<(const TaggedString &that) const; - QString str; - // Type will help decide the lifetime of the tagged strings + QString string; Type type; - - mutable std::chrono::steady_clock::time_point timeAdded; }; public: - CompletionModel(const QString &channelName); + CompletionModel(Channel &channel); virtual int columnCount(const QModelIndex &) const override; virtual QVariant data(const QModelIndex &index, int) const override; virtual int rowCount(const QModelIndex &) const override; - void refresh(); - void addString(const QString &str, TaggedString::Type type); - void addUser(const QString &str); - - void clearExpiredStrings(); + void refresh(const QString &prefix); private: TaggedString createUser(const QString &str); - mutable std::mutex emotesMutex_; - std::set emotes_; - - QString channelName_; + std::set items_; + mutable std::mutex itemsMutex_; + Channel &channel_; }; } // namespace chatterino diff --git a/src/common/Emotemap.cpp b/src/common/Emotemap.cpp deleted file mode 100644 index 9bfae628b..000000000 --- a/src/common/Emotemap.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "Emotemap.hpp" - -#include "Application.hpp" -#include "singletons/Settings.hpp" - -namespace chatterino { - -// EmoteData::EmoteData(Image *image) -// : image1x(image) -//{ -//} - -//// Emotes must have a 1x image to be valid -// bool EmoteData::isValid() const -//{ -// return this->image1x != nullptr; -//} - -// Image *EmoteData::getImage(float scale) const -//{ -// int quality = getApp()->settings->preferredEmoteQuality; - -// if (quality == 0) { -// scale *= getApp()->settings->emoteScale.getValue(); -// quality = [&] { -// if (scale <= 1) -// return 1; -// if (scale <= 2) -// return 2; -// return 3; -// }(); -// } - -// Image *_image; -// if (quality == 3 && this->image3x != nullptr) { -// _image = this->image3x; -// } else if (quality >= 2 && this->image2x != nullptr) { -// _image = this->image2x; -// } else { -// _image = this->image1x; -// } - -// return _image; -//} - -} // namespace chatterino diff --git a/src/common/Emotemap.hpp b/src/common/Emotemap.hpp deleted file mode 100644 index 8c219bc67..000000000 --- a/src/common/Emotemap.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "messages/Image.hpp" -#include "util/ConcurrentMap.hpp" - -namespace chatterino { - -// struct EmoteData { -// EmoteData() = default; - -// EmoteData(Image *image); - -// // Emotes must have a 1x image to be valid -// bool isValid() const; -// Image *getImage(float scale) const; - -// // Link to the emote page i.e. -// https://www.frankerfacez.com/emoticon/144722-pajaCringe QString pageLink; - -// Image *image1x = nullptr; -// Image *image2x = nullptr; -// Image *image3x = nullptr; -//}; - -// using EmoteMap = ConcurrentMap; - -} // namespace chatterino diff --git a/src/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp index 000c3da70..c5be885e5 100644 --- a/src/common/FlagsEnum.hpp +++ b/src/common/FlagsEnum.hpp @@ -4,19 +4,17 @@ namespace chatterino { -// = std::enable_if::value>::type - template ::type> class FlagsEnum { public: FlagsEnum() - : value(static_cast(0)) + : value_(static_cast(0)) { } FlagsEnum(T value) - : value(value) + : value_(value) { } @@ -29,22 +27,22 @@ public: bool operator==(const FlagsEnum &other) { - return this->value == other.value; + return this->value_ == other.value_; } bool operator!=(const FlagsEnum &other) { - return this->value != other.value; + return this->value_ != other.value_; } void set(T flag) { - reinterpret_cast(this->value) |= static_cast(flag); + reinterpret_cast(this->value_) |= static_cast(flag); } void unset(T flag) { - reinterpret_cast(this->value) &= ~static_cast(flag); + reinterpret_cast(this->value_) &= ~static_cast(flag); } void set(T flag, bool value) @@ -57,33 +55,17 @@ public: bool has(T flag) const { - return static_cast(this->value) & static_cast(flag); + return static_cast(this->value_) & static_cast(flag); } - // bool hasAny(std::initializer_list flags) const - //{ - // for (auto flag : flags) { - // if (this->has(flag)) return true; - // } - // return false; - //} - bool hasAny(FlagsEnum flags) const { - return static_cast(this->value) & static_cast(flags.value); + return static_cast(this->value_) & static_cast(flags.value_); } - // bool hasAll(std::initializer_list flags) const - //{ - // for (auto flag : flags) { - // if (!this->has(flag)) return false; - // } - // return true; - //} - bool hasAll(FlagsEnum flags) const { - return (static_cast(this->value) & static_cast(flags.value)) && + return (static_cast(this->value_) & static_cast(flags.value_)) && static_cast(flags->value); } @@ -93,7 +75,7 @@ public: } private: - T value; + T value_{}; }; } // namespace chatterino diff --git a/src/common/LinkParser.cpp b/src/common/LinkParser.cpp index b4f63a156..a80c75641 100644 --- a/src/common/LinkParser.cpp +++ b/src/common/LinkParser.cpp @@ -5,69 +5,63 @@ #include #include +// ip 0.0.0.0 - 224.0.0.0 +#define IP \ + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" \ + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" \ + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +#define PORT "(?::\\d{2,5})" +#define WEB_CHAR1 "[_a-z\\x{00a1}-\\x{ffff}0-9]" +#define WEB_CHAR2 "[a-z\\x{00a1}-\\x{ffff}0-9]" + +#define SPOTIFY_1 "(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+" +#define SPOTIFY_2 "user:[^:]+" +#define SPOTIFY_3 "search:(?:[-\\w$\\.+!*'(),]+|%[a-fA-F0-9]{2})+" +#define SPOTIFY_PARAMS "(?:" SPOTIFY_1 "|" SPOTIFY_2 "|" SPOTIFY_3 ")" +#define SPOTIFY_LINK "(?x-mi:(spotify:" SPOTIFY_PARAMS "))" + +#define WEB_PROTOCOL "(?:(?:https?|ftps?)://)?" +#define WEB_USER "(?:\\S+(?::\\S*)?@)?" +#define WEB_HOST "(?:(?:" WEB_CHAR1 "-*)*" WEB_CHAR2 "+)" +#define WEB_DOMAIN "(?:\\.(?:" WEB_CHAR2 "-*)*" WEB_CHAR2 "+)*" +#define WEB_TLD "(?:" + tldData + ")" +#define WEB_RESOURCE_PATH "(?:[/?#]\\S*)" +#define WEB_LINK \ + WEB_PROTOCOL WEB_USER "(?:" IP "|" WEB_HOST WEB_DOMAIN "\\." WEB_TLD PORT \ + "?" WEB_RESOURCE_PATH "?)" + +#define LINK "^(?:" SPOTIFY_LINK "|" WEB_LINK ")$" + namespace chatterino { LinkParser::LinkParser(const QString &unparsedString) { static QRegularExpression linkRegex = [] { static QRegularExpression newLineRegex("\r?\n"); - QFile tldFile(":/tlds.txt"); - tldFile.open(QFile::ReadOnly); + QFile file(":/tlds.txt"); + file.open(QFile::ReadOnly); + QTextStream tlds(&file); + tlds.setCodec("UTF-8"); - QTextStream t1(&tldFile); - t1.setCodec("UTF-8"); + // tldData gets injected into the LINK macro + auto tldData = tlds.readAll().replace(newLineRegex, "|"); + (void)tldData; - // Read the TLDs in and replace the newlines with pipes - QString tldData = t1.readAll().replace(newLineRegex, "|"); - - const QString hyperlinkRegExp = - "^" - // Identifier for spotify - "(?x-mi:(spotify:(?:" - "(?:artist|album|track|user:[^:]+:playlist):" - "[a-zA-Z0-9]+|user:[^:]+|search:" - "(?:[-\\w$\\.+!*'(),]+|%[a-fA-F0-9]{2})+)))" - // If nothing matches then just go on - "|" - "^" - // Identifier for http and ftp - "(?:(?:https?|ftps?)://)?" - // user:pass authentication - "(?:\\S+(?::\\S*)?@)?" - "(?:" - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broacast addresses - // (first & last IP address of each class) - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" - "|" - // host name - "(?:(?:[_a-z\\x{00a1}-\\x{ffff}0-9]-*)*[a-z\\x{00a1}-\\x{ffff}0-9]+" - ")" - // domain name - "(?:\\.(?:[a-z\\x{00a1}-\\x{ffff}0-9]-*)*[a-z\\x{00a1}-\\x{ffff}0-" - "9]+)*" - // TLD identifier - //"(?:\\.(?:[a-z\\x{00a1}-\\x{ffff}]{2,}))" - "(?:[\\.](?:" + - tldData + - "))" - "\\.?" - ")" - // port number - "(?::\\d{2,5})?" - // resource path - "(?:[/?#]\\S*)?" - "$"; - - return QRegularExpression(hyperlinkRegExp, + return QRegularExpression(LINK, QRegularExpression::CaseInsensitiveOption); }(); this->match_ = linkRegex.match(unparsedString); } +bool LinkParser::hasMatch() const +{ + return this->match_.hasMatch(); +} + +QString LinkParser::getCaptured() const +{ + return this->match_.captured(); +} + } // namespace chatterino diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 3a1ad6fab..2d56cbde0 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -10,15 +10,8 @@ class LinkParser public: explicit LinkParser(const QString &unparsedString); - bool hasMatch() const - { - return this->match_.hasMatch(); - } - - QString getCaptured() const - { - return this->match_.captured(); - } + bool hasMatch() const; + QString getCaptured() const; private: QRegularExpressionMatch match_; diff --git a/src/common/LockedObject.hpp b/src/common/LockedObject.hpp deleted file mode 100644 index 01ffe3b66..000000000 --- a/src/common/LockedObject.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -template -class LockedObject -{ -public: - LockedObject &operator=(const LockedObject &other) - { - this->mutex_.lock(); - - this->data = other.getValue(); - - this->mutex_.unlock(); - - return *this; - } - - LockedObject &operator=(const Type &other) - { - this->mutex_.lock(); - - this->data = other; - - this->mutex_.unlock(); - - return *this; - } - -private: - Type value_; - std::mutex mutex_; -}; - -} // namespace chatterino diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 30d1e9267..b214f5472 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -2,12 +2,11 @@ #include -#include "Common.hpp" - class QNetworkReply; namespace chatterino { +class Outcome; class NetworkResult; using NetworkSuccessCallback = std::function; diff --git a/src/common/NetworkData.cpp b/src/common/NetworkData.cpp index ad8fe40b2..a58ec7076 100644 --- a/src/common/NetworkData.cpp +++ b/src/common/NetworkData.cpp @@ -1,6 +1,5 @@ #include "common/NetworkData.hpp" -#include "Application.hpp" #include "singletons/Paths.hpp" #include "util/DebugCount.hpp" @@ -42,9 +41,7 @@ QString NetworkData::getHash() void NetworkData::writeToCache(const QByteArray &bytes) { if (this->useQuickLoadCache_) { - auto app = getApp(); - - QFile cachedFile(app->paths->cacheDirectory + "/" + this->getHash()); + QFile cachedFile(getPaths()->cacheDirectory() + "/" + this->getHash()); if (cachedFile.open(QIODevice::WriteOnly)) { cachedFile.write(bytes); diff --git a/src/common/NetworkData.hpp b/src/common/NetworkData.hpp index d7fbf84ba..2d149fc09 100644 --- a/src/common/NetworkData.hpp +++ b/src/common/NetworkData.hpp @@ -19,6 +19,7 @@ struct NetworkData { QNetworkRequest request_; const QObject *caller_ = nullptr; bool useQuickLoadCache_{}; + bool executeConcurrently{}; NetworkReplyCreatedCallback onReplyCreated_; NetworkErrorCallback onError_; diff --git a/src/common/NetworkManager.cpp b/src/common/NetworkManager.cpp index 5577afda5..1ff1bf635 100644 --- a/src/common/NetworkManager.cpp +++ b/src/common/NetworkManager.cpp @@ -5,11 +5,11 @@ namespace chatterino { QThread NetworkManager::workerThread; -QNetworkAccessManager NetworkManager::NaM; +QNetworkAccessManager NetworkManager::accessManager; void NetworkManager::init() { - NetworkManager::NaM.moveToThread(&NetworkManager::workerThread); + NetworkManager::accessManager.moveToThread(&NetworkManager::workerThread); NetworkManager::workerThread.start(); } diff --git a/src/common/NetworkManager.hpp b/src/common/NetworkManager.hpp index 0e59c0540..530aaae1f 100644 --- a/src/common/NetworkManager.hpp +++ b/src/common/NetworkManager.hpp @@ -1,16 +1,7 @@ #pragma once -#include "common/NetworkRequester.hpp" -#include "common/NetworkWorker.hpp" -#include "debug/Log.hpp" - -#include -#include #include -#include #include -#include -#include namespace chatterino { @@ -20,7 +11,7 @@ class NetworkManager : public QObject public: static QThread workerThread; - static QNetworkAccessManager NaM; + static QNetworkAccessManager accessManager; static void init(); static void deinit(); diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index 352762c3f..024f11a4b 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -1,13 +1,16 @@ #include "common/NetworkRequest.hpp" -#include "Application.hpp" +#include "common/NetworkData.hpp" #include "common/NetworkManager.hpp" +#include "common/Outcome.hpp" #include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "singletons/Paths.hpp" #include "util/DebugCount.hpp" #include +#include + #include namespace chatterino { @@ -80,6 +83,11 @@ void NetworkRequest::setTimeout(int ms) this->timer->timeoutMS_ = ms; } +void NetworkRequest::setExecuteConcurrently(bool value) +{ + this->data->executeConcurrently = value; +} + void NetworkRequest::makeAuthorizedV5(const QString &clientID, const QString &oauthToken) { @@ -130,16 +138,15 @@ void NetworkRequest::execute() } break; default: { - Log("[Execute] Unhandled request type"); + log("[Execute] Unhandled request type"); } break; } } Outcome NetworkRequest::tryLoadCachedFile() { - auto app = getApp(); - - QFile cachedFile(app->paths->cacheDirectory + "/" + this->data->getHash()); + QFile cachedFile(getPaths()->cacheDirectory() + "/" + + this->data->getHash()); if (!cachedFile.exists()) { // File didn't exist @@ -180,14 +187,15 @@ void NetworkRequest::doRequest() auto reply = [&]() -> QNetworkReply * { switch (data->requestType_) { case NetworkRequestType::Get: - return NetworkManager::NaM.get(data->request_); + return NetworkManager::accessManager.get(data->request_); case NetworkRequestType::Put: - return NetworkManager::NaM.put(data->request_, - data->payload_); + return NetworkManager::accessManager.put(data->request_, + data->payload_); case NetworkRequestType::Delete: - return NetworkManager::NaM.deleteResource(data->request_); + return NetworkManager::accessManager.deleteResource( + data->request_); default: return nullptr; @@ -195,13 +203,13 @@ void NetworkRequest::doRequest() }(); if (reply == nullptr) { - Log("Unhandled request type"); + log("Unhandled request type"); return; } if (timer->isStarted()) { timer->onTimeout(worker, [reply, data]() { - Log("Aborted!"); + log("Aborted!"); reply->abort(); if (data->onError_) { data->onError_(-2); @@ -228,7 +236,16 @@ void NetworkRequest::doRequest() NetworkResult result(bytes); DebugCount::increase("http request success"); - data->onSuccess_(result); + // log("starting {}", data->request_.url().toString()); + if (data->onSuccess_) { + if (data->executeConcurrently) + QtConcurrent::run( + [onSuccess = std::move(data->onSuccess_), + result = std::move(result)] { onSuccess(result); }); + else + data->onSuccess_(result); + } + // log("finished {}", data->request_.url().toString()); reply->deleteLater(); }; diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp index 8112a4852..f1940621d 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/NetworkRequest.hpp @@ -1,14 +1,13 @@ #pragma once -#include "Application.hpp" #include "common/NetworkCommon.hpp" -#include "common/NetworkData.hpp" #include "common/NetworkRequester.hpp" #include "common/NetworkResult.hpp" #include "common/NetworkTimer.hpp" #include "common/NetworkWorker.hpp" namespace chatterino { +class NetworkData; class NetworkRequest { @@ -27,19 +26,15 @@ class NetworkRequest bool executed_ = false; public: - NetworkRequest() = delete; - NetworkRequest(const NetworkRequest &other) = delete; - NetworkRequest &operator=(const NetworkRequest &other) = delete; - - NetworkRequest(NetworkRequest &&other) = default; - NetworkRequest &operator=(NetworkRequest &&other) = default; - explicit NetworkRequest( const std::string &url, NetworkRequestType requestType = NetworkRequestType::Get); explicit NetworkRequest( QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); + NetworkRequest(NetworkRequest &&other) = default; + NetworkRequest &operator=(NetworkRequest &&other) = default; + ~NetworkRequest(); void setRequestType(NetworkRequestType newRequestType); @@ -55,14 +50,13 @@ public: void setRawHeader(const char *headerName, const QByteArray &value); void setRawHeader(const char *headerName, const QString &value); void setTimeout(int ms); + void setExecuteConcurrently(bool value); void makeAuthorizedV5(const QString &clientID, const QString &oauthToken = QString()); void execute(); private: - // Returns true if the file was successfully loaded from cache - // Returns false if the cache file either didn't exist, or it contained // "invalid" data "invalid" is specified by the onSuccess callback Outcome tryLoadCachedFile(); diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp index 443d0aa05..78884a421 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/NetworkResult.cpp @@ -31,7 +31,7 @@ rapidjson::Document NetworkResult::parseRapidJson() const ret.Parse(this->data_.data(), this->data_.length()); if (result.Code() != rapidjson::kParseErrorNone) { - Log("JSON parse error: {} ({})", + log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), result.Offset()); return ret; } diff --git a/src/common/Property.hpp b/src/common/Property.hpp deleted file mode 100644 index 99d0efa48..000000000 --- a/src/common/Property.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "boost/noncopyable.hpp" - -namespace chatterino { - -template -class Property final : boost::noncopyable -{ -public: - Property() - { - } - - Property(const T &value) - : value_(value) - { - } - - T &operator=(const T &f) - { - return value_ = f; - } - - operator T const &() const - { - return value_; - } - -protected: - T value_; -}; - -} // namespace chatterino diff --git a/src/common/SerializeCustom.hpp b/src/common/SerializeCustom.hpp deleted file mode 100644 index 754cbf115..000000000 --- a/src/common/SerializeCustom.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -namespace pajlada { -namespace Settings { - -template <> -struct Serialize { - static rapidjson::Value get(const QString &value, - rapidjson::Document::AllocatorType &a) - { - return rapidjson::Value(value.toUtf8(), a); - } -}; - -template <> -struct Deserialize { - static QString get(const rapidjson::Value &value, bool *error = nullptr) - { - if (!value.IsString()) { - PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION( - "Deserialized rapidjson::Value is not a string"); - return QString{}; - } - - try { - return QString::fromUtf8(value.GetString(), - value.GetStringLength()); - } catch (const std::exception &) { - // int x = 5; - } catch (...) { - // int y = 5; - } - - return QString{}; - } -}; - -} // namespace Settings -} // namespace pajlada diff --git a/src/common/SimpleSignalVector.hpp b/src/common/SimpleSignalVector.hpp deleted file mode 100644 index 6aca458f5..000000000 --- a/src/common/SimpleSignalVector.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace chatterino { - -template -class SimpleSignalVector -{ -public: - SimpleSignalVector &operator=(std::vector &other) - { - this->data_ = other; - - this->updated.invoke(); - - return *this; - } - - operator std::vector &() - { - return this->data_; - } - - pajlada::Signals::NoArgSignal updated; - -private: - std::vector data_; -}; - -} // namespace chatterino diff --git a/src/common/UniqueAccess.hpp b/src/common/UniqueAccess.hpp index 529ac0623..30d6bee32 100644 --- a/src/common/UniqueAccess.hpp +++ b/src/common/UniqueAccess.hpp @@ -46,16 +46,15 @@ public: } private: - T *element_; - std::mutex *mutex_; - bool isValid_ = true; + T *element_{}; + std::mutex *mutex_{}; + bool isValid_{true}; }; template class UniqueAccess { public: - // template UniqueAccess() : element_(T()) { @@ -88,7 +87,8 @@ public: return AccessGuard(this->element_, this->mutex_); } - template >> + template ::value>> AccessGuard accessConst() const { return AccessGuard(this->element_, this->mutex_); diff --git a/src/common/UsernameSet.cpp b/src/common/UsernameSet.cpp new file mode 100644 index 000000000..064b6cf54 --- /dev/null +++ b/src/common/UsernameSet.cpp @@ -0,0 +1,112 @@ +#include "UsernameSet.hpp" + +#include + +namespace chatterino { + +// +// UsernameSet +// + +UsernameSet::ConstIterator UsernameSet::begin() const +{ + return this->items.begin(); +} + +UsernameSet::ConstIterator UsernameSet::end() const +{ + return this->items.end(); +} + +UsernameSet::Range UsernameSet::subrange(const Prefix &prefix) const +{ + auto it = this->firstKeyForPrefix.find(prefix); + if (it != this->firstKeyForPrefix.end()) { + auto start = this->items.find(it->second); + auto end = start; + + while (end != this->items.end() && prefix.isStartOf(*end)) { + end++; + } + return {start, end}; + } + + return {this->items.end(), this->items.end()}; +} + +std::set::size_type UsernameSet::size() const +{ + return this->items.size(); +} + +std::pair UsernameSet::insert(const QString &value) +{ + this->insertPrefix(value); + + return this->items.insert(value); +} + +std::pair UsernameSet::insert(QString &&value) +{ + this->insertPrefix(value); + + return this->items.insert(std::move(value)); +} + +void UsernameSet::insertPrefix(const QString &value) +{ + auto &string = this->firstKeyForPrefix[Prefix(value)]; + + if (string.isNull() || value < string) string = value; +} + +// +// Range +// + +UsernameSet::Range::Range(ConstIterator start, ConstIterator end) + : start_(start) + , end_(end) +{ +} + +UsernameSet::ConstIterator UsernameSet::Range::begin() +{ + return this->start_; +} + +UsernameSet::ConstIterator UsernameSet::Range::end() +{ + return this->end_; +} + +// +// Prefix +// + +Prefix::Prefix(const QString &string) + : first(string.size() >= 1 ? string[0].toLower() : '\0') + , second(string.size() >= 2 ? string[1].toLower() : '\0') +{ +} + +bool Prefix::operator==(const Prefix &other) const +{ + return std::tie(this->first, this->second) == + std::tie(other.first, other.second); +} + +bool Prefix::isStartOf(const QString &string) const +{ + if (string.size() == 0) { + return this->first == QChar('\0') && this->second == QChar('\0'); + } else if (string.size() == 1) { + return this->first == string[0].toLower() && + this->second == QChar('\0'); + } else { + return this->first == string[0].toLower() && + this->second == string[1].toLower(); + } +} + +} // namespace chatterino diff --git a/src/common/UsernameSet.hpp b/src/common/UsernameSet.hpp new file mode 100644 index 000000000..d71bcdaf6 --- /dev/null +++ b/src/common/UsernameSet.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +namespace chatterino { +class Prefix +{ +public: + Prefix(const QString &string); + bool operator==(const Prefix &other) const; + bool isStartOf(const QString &string) const; + +private: + QChar first; + QChar second; + + friend struct std::hash; +}; +} // namespace chatterino + +namespace std { +template <> +struct hash { + size_t operator()(const chatterino::Prefix &prefix) const + { + return (size_t(prefix.first.unicode()) << 16) | + size_t(prefix.second.unicode()); + } +}; +} // namespace std + +namespace chatterino { +class UsernameSet +{ +public: + static constexpr int PrefixLength = 2; + + using Iterator = std::set::iterator; + using ConstIterator = std::set::const_iterator; + + class Range + { + public: + Range(ConstIterator start, ConstIterator end); + + ConstIterator begin(); + ConstIterator end(); + + private: + ConstIterator start_; + ConstIterator end_; + }; + + ConstIterator begin() const; + ConstIterator end() const; + Range subrange(const Prefix &prefix) const; + + std::set::size_type size() const; + + std::pair insert(const QString &value); + std::pair insert(QString &&value); + +private: + void insertPrefix(const QString &string); + + std::set items; + std::unordered_map firstKeyForPrefix; +}; + +} // namespace chatterino diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 2b1b51b39..4101f3a56 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -5,9 +5,9 @@ #define CHATTERINO_VERSION "2.0.4" #if defined(Q_OS_WIN) -#define CHATTERINO_OS "win" +# define CHATTERINO_OS "win" #elif defined(Q_OS_MACOS) -#define CHATTERINO_OS "macos" +# define CHATTERINO_OS "macos" #elif defined(Q_OS_LINUX) -#define CHATTERINO_OS "linux" +# define CHATTERINO_OS "linux" #endif diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 6cd1f2558..7d2c84266 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -1,6 +1,8 @@ #include "AccountController.hpp" +#include "controllers/accounts/Account.hpp" #include "controllers/accounts/AccountModel.hpp" +#include "providers/twitch/TwitchAccount.hpp" namespace chatterino { diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp index 023ad9c9e..b47ec86be 100644 --- a/src/controllers/accounts/AccountController.hpp +++ b/src/controllers/accounts/AccountController.hpp @@ -1,16 +1,15 @@ #pragma once -#include "common/Singleton.hpp" - #include #include "common/SignalVector.hpp" -#include "controllers/accounts/Account.hpp" +#include "common/Singleton.hpp" #include "providers/twitch/TwitchAccountManager.hpp" #include "util/SharedPtrElementLess.hpp" namespace chatterino { +class Account; class Settings; class Paths; diff --git a/src/controllers/accounts/AccountModel.cpp b/src/controllers/accounts/AccountModel.cpp index d63f2c288..23ded2881 100644 --- a/src/controllers/accounts/AccountModel.cpp +++ b/src/controllers/accounts/AccountModel.cpp @@ -1,5 +1,6 @@ #include "AccountModel.hpp" +#include "controllers/accounts/Account.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { diff --git a/src/controllers/accounts/AccountModel.hpp b/src/controllers/accounts/AccountModel.hpp index c0b4538d5..a8b020410 100644 --- a/src/controllers/accounts/AccountModel.hpp +++ b/src/controllers/accounts/AccountModel.hpp @@ -8,6 +8,7 @@ namespace chatterino { +class Account; class AccountController; class AccountModel : public SignalVectorModel> diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 111c06786..f931aa073 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -5,7 +5,10 @@ #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandModel.hpp" +#include "debug/Log.hpp" +#include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" #include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchServer.hpp" @@ -78,7 +81,7 @@ void CommandController::save() { QFile textFile(this->filePath_); if (!textFile.open(QIODevice::WriteOnly)) { - Log("[CommandController::saveCommands] Unable to open {} for writing", + log("[CommandController::saveCommands] Unable to open {} for writing", this->filePath_); return; } diff --git a/src/controllers/highlights/HighlightBlacklistUser.hpp b/src/controllers/highlights/HighlightBlacklistUser.hpp index 5376d9cf5..b2e3e4a06 100644 --- a/src/controllers/highlights/HighlightBlacklistUser.hpp +++ b/src/controllers/highlights/HighlightBlacklistUser.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -68,37 +68,39 @@ private: namespace pajlada { namespace Settings { -template <> -struct Serialize { - static rapidjson::Value get(const chatterino::HighlightBlacklistUser &value, - rapidjson::Document::AllocatorType &a) - { - rapidjson::Value ret(rapidjson::kObjectType); + template <> + struct Serialize { + static rapidjson::Value get( + const chatterino::HighlightBlacklistUser &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); - AddMember(ret, "pattern", value.getPattern(), a); - AddMember(ret, "regex", value.isRegex(), a); + AddMember(ret, "pattern", value.getPattern(), a); + AddMember(ret, "regex", value.isRegex(), a); - return ret; - } -}; + return ret; + } + }; -template <> -struct Deserialize { - static chatterino::HighlightBlacklistUser get(const rapidjson::Value &value) - { - QString pattern; - bool isRegex = false; + template <> + struct Deserialize { + static chatterino::HighlightBlacklistUser get( + const rapidjson::Value &value) + { + QString pattern; + bool isRegex = false; + + if (!value.IsObject()) { + return chatterino::HighlightBlacklistUser(pattern, isRegex); + } + + chatterino::rj::getSafe(value, "pattern", pattern); + chatterino::rj::getSafe(value, "regex", isRegex); - if (!value.IsObject()) { return chatterino::HighlightBlacklistUser(pattern, isRegex); } - - chatterino::rj::getSafe(value, "pattern", pattern); - chatterino::rj::getSafe(value, "regex", isRegex); - - return chatterino::HighlightBlacklistUser(pattern, isRegex); - } -}; + }; } // namespace Settings } // namespace pajlada diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp index 09d686cb2..963f23aaa 100644 --- a/src/controllers/highlights/HighlightController.hpp +++ b/src/controllers/highlights/HighlightController.hpp @@ -1,15 +1,16 @@ #pragma once -#include "common/Singleton.hpp" - +#include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" +#include "common/Singleton.hpp" #include "controllers/highlights/HighlightBlacklistUser.hpp" #include "controllers/highlights/HighlightPhrase.hpp" -#include "messages/Message.hpp" -#include "singletons/Settings.hpp" namespace chatterino { +struct Message; +using MessagePtr = std::shared_ptr; + class Settings; class Paths; diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index cad2fd006..827e23a4a 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -37,13 +37,13 @@ void HighlightModel::getRowFromItem(const HighlightPhrase &item, void HighlightModel::afterInit() { std::vector row = this->createRow(); - setBoolItem(row[0], getApp()->settings->enableHighlightsSelf.getValue(), - true, false); + setBoolItem(row[0], getSettings()->enableHighlightsSelf.getValue(), true, + false); row[0]->setData("Your username (automatic)", Qt::DisplayRole); - setBoolItem(row[1], getApp()->settings->enableHighlightTaskbar.getValue(), - true, false); - setBoolItem(row[2], getApp()->settings->enableHighlightSound.getValue(), - true, false); + setBoolItem(row[1], getSettings()->enableHighlightTaskbar.getValue(), true, + false); + setBoolItem(row[2], getSettings()->enableHighlightSound.getValue(), true, + false); row[3]->setFlags(0); this->insertCustomRow(row, 0); } @@ -55,20 +55,17 @@ void HighlightModel::customRowSetData(const std::vector &row, switch (column) { case 0: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightsSelf.setValue( - value.toBool()); + getSettings()->enableHighlightsSelf.setValue(value.toBool()); } } break; case 1: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightTaskbar.setValue( - value.toBool()); + getSettings()->enableHighlightTaskbar.setValue(value.toBool()); } } break; case 2: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightSound.setValue( - value.toBool()); + getSettings()->enableHighlightSound.setValue(value.toBool()); } } break; case 3: { diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index 77cd5936b..cfbee6588 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -72,43 +72,45 @@ private: namespace pajlada { namespace Settings { -template <> -struct Serialize { - static rapidjson::Value get(const chatterino::HighlightPhrase &value, - rapidjson::Document::AllocatorType &a) - { - rapidjson::Value ret(rapidjson::kObjectType); + template <> + struct Serialize { + static rapidjson::Value get(const chatterino::HighlightPhrase &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); - AddMember(ret, "pattern", value.getPattern(), a); - AddMember(ret, "alert", value.getAlert(), a); - AddMember(ret, "sound", value.getSound(), a); - AddMember(ret, "regex", value.isRegex(), a); + AddMember(ret, "pattern", value.getPattern(), a); + AddMember(ret, "alert", value.getAlert(), a); + AddMember(ret, "sound", value.getSound(), a); + AddMember(ret, "regex", value.isRegex(), a); - return ret; - } -}; - -template <> -struct Deserialize { - static chatterino::HighlightPhrase get(const rapidjson::Value &value) - { - if (!value.IsObject()) { - return chatterino::HighlightPhrase(QString(), true, false, false); + return ret; } + }; - QString _pattern; - bool _alert = true; - bool _sound = false; - bool _isRegex = false; + template <> + struct Deserialize { + static chatterino::HighlightPhrase get(const rapidjson::Value &value) + { + if (!value.IsObject()) { + return chatterino::HighlightPhrase(QString(), true, false, + 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); + QString _pattern; + bool _alert = true; + bool _sound = false; + bool _isRegex = false; - return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex); - } -}; + chatterino::rj::getSafe(value, "pattern", _pattern); + chatterino::rj::getSafe(value, "alert", _alert); + chatterino::rj::getSafe(value, "sound", _sound); + chatterino::rj::getSafe(value, "regex", _isRegex); + + return chatterino::HighlightPhrase(_pattern, _alert, _sound, + _isRegex); + } + }; } // namespace Settings } // namespace pajlada diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index e7a0472e6..10c83ac10 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -1,10 +1,9 @@ #pragma once -#include "common/Singleton.hpp" - +#include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" +#include "common/Singleton.hpp" #include "controllers/ignores/IgnorePhrase.hpp" -#include "singletons/Settings.hpp" namespace chatterino { diff --git a/src/controllers/ignores/IgnorePhrase.hpp b/src/controllers/ignores/IgnorePhrase.hpp index f42df8442..16601708a 100644 --- a/src/controllers/ignores/IgnorePhrase.hpp +++ b/src/controllers/ignores/IgnorePhrase.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -59,37 +59,37 @@ private: namespace pajlada { namespace Settings { -template <> -struct Serialize { - static rapidjson::Value get(const chatterino::IgnorePhrase &value, - rapidjson::Document::AllocatorType &a) - { - rapidjson::Value ret(rapidjson::kObjectType); + template <> + struct Serialize { + static rapidjson::Value get(const chatterino::IgnorePhrase &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); - AddMember(ret, "pattern", value.getPattern(), a); - AddMember(ret, "regex", value.isRegex(), a); + AddMember(ret, "pattern", value.getPattern(), a); + AddMember(ret, "regex", value.isRegex(), a); - return ret; - } -}; - -template <> -struct Deserialize { - static chatterino::IgnorePhrase get(const rapidjson::Value &value) - { - if (!value.IsObject()) { - return chatterino::IgnorePhrase(QString(), false); + return ret; } + }; - QString _pattern; - bool _isRegex = false; + template <> + struct Deserialize { + static chatterino::IgnorePhrase get(const rapidjson::Value &value) + { + if (!value.IsObject()) { + return chatterino::IgnorePhrase(QString(), false); + } - chatterino::rj::getSafe(value, "pattern", _pattern); - chatterino::rj::getSafe(value, "regex", _isRegex); + QString _pattern; + bool _isRegex = false; - return chatterino::IgnorePhrase(_pattern, _isRegex); - } -}; + chatterino::rj::getSafe(value, "pattern", _pattern); + chatterino::rj::getSafe(value, "regex", _isRegex); + + return chatterino::IgnorePhrase(_pattern, _isRegex); + } + }; } // namespace Settings } // namespace pajlada diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index 90abe815d..cc2b8463e 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -2,6 +2,7 @@ #include #include "Application.hpp" +#include "messages/Image.hpp" #include "singletons/Resources.hpp" namespace chatterino { @@ -60,8 +61,7 @@ ModerationAction::ModerationAction(const QString &action) // str); // } } else if (action.startsWith("/ban ")) { - this->image_ = - Image::fromNonOwningPixmap(&getApp()->resources->buttons.ban); + this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban); } else { QString xD = action; diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index f509f79c5..8dbdba93f 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -4,11 +4,13 @@ #include #include -#include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { +class Image; +using ImagePtr = std::shared_ptr; + class ModerationAction { public: @@ -34,34 +36,34 @@ private: namespace pajlada { namespace Settings { -template <> -struct Serialize { - static rapidjson::Value get(const chatterino::ModerationAction &value, - rapidjson::Document::AllocatorType &a) - { - rapidjson::Value ret(rapidjson::kObjectType); + template <> + struct Serialize { + static rapidjson::Value get(const chatterino::ModerationAction &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); - AddMember(ret, "pattern", value.getAction(), a); + AddMember(ret, "pattern", value.getAction(), a); - return ret; - } -}; - -template <> -struct Deserialize { - static chatterino::ModerationAction get(const rapidjson::Value &value) - { - if (!value.IsObject()) { - return chatterino::ModerationAction(QString()); + return ret; } + }; - QString pattern; + template <> + struct Deserialize { + static chatterino::ModerationAction get(const rapidjson::Value &value) + { + if (!value.IsObject()) { + return chatterino::ModerationAction(QString()); + } - chatterino::rj::getSafe(value, "pattern", pattern); + QString pattern; - return chatterino::ModerationAction(pattern); - } -}; + chatterino::rj::getSafe(value, "pattern", pattern); + + return chatterino::ModerationAction(pattern); + } + }; } // namespace Settings } // namespace pajlada diff --git a/src/controllers/moderationactions/ModerationActions.cpp b/src/controllers/moderationactions/ModerationActions.cpp index 68070f6b2..27cd5de30 100644 --- a/src/controllers/moderationactions/ModerationActions.cpp +++ b/src/controllers/moderationactions/ModerationActions.cpp @@ -17,12 +17,16 @@ void ModerationActions::initialize(Settings &settings, Paths &paths) assert(!this->initialized_); this->initialized_ = true; - for (auto &val : this->setting_.getValue()) { + this->setting_ = + std::make_unique>>( + "/moderation/actions"); + + for (auto &val : this->setting_->getValue()) { this->items.insertItem(val); } this->items.delayedItemsChanged.connect([this] { // - this->setting_.setValue(this->items.getVector()); + this->setting_->setValue(this->items.getVector()); }); } diff --git a/src/controllers/moderationactions/ModerationActions.hpp b/src/controllers/moderationactions/ModerationActions.hpp index d5e510f1c..08314eb22 100644 --- a/src/controllers/moderationactions/ModerationActions.hpp +++ b/src/controllers/moderationactions/ModerationActions.hpp @@ -25,8 +25,7 @@ public: ModerationActionModel *createModel(QObject *parent); private: - ChatterinoSetting> setting_ = { - "/moderation/actions"}; + std::unique_ptr>> setting_; bool initialized_ = false; }; diff --git a/src/controllers/taggedusers/TaggedUsersModel.cpp b/src/controllers/taggedusers/TaggedUsersModel.cpp index e3225888c..037b4ae63 100644 --- a/src/controllers/taggedusers/TaggedUsersModel.cpp +++ b/src/controllers/taggedusers/TaggedUsersModel.cpp @@ -29,12 +29,12 @@ void TaggedUsersModel::afterInit() { // std::vector row = this->createRow(); // setBoolItem(row[0], - // getApp()->settings->enableHighlightsSelf.getValue(), true, false); + // getSettings()->enableHighlightsSelf.getValue(), true, false); // row[0]->setData("Your username (automatic)", Qt::DisplayRole); // setBoolItem(row[1], - // getApp()->settings->enableHighlightTaskbar.getValue(), true, false); + // getSettings()->enableHighlightTaskbar.getValue(), true, false); // setBoolItem(row[2], - // getApp()->settings->enableHighlightSound.getValue(), true, false); + // getSettings()->enableHighlightSound.getValue(), true, false); // row[3]->setFlags(0); this->insertCustomRow(row, 0); } @@ -45,17 +45,17 @@ void TaggedUsersModel::afterInit() // switch (column) { // case 0: { // if (role == Qt::CheckStateRole) { -// getApp()->settings->enableHighlightsSelf.setValue(value.toBool()); +// getSettings()->enableHighlightsSelf.setValue(value.toBool()); // } // } break; // case 1: { // if (role == Qt::CheckStateRole) { -// getApp()->settings->enableHighlightTaskbar.setValue(value.toBool()); +// getSettings()->enableHighlightTaskbar.setValue(value.toBool()); // } // } break; // case 2: { // if (role == Qt::CheckStateRole) { -// getApp()->settings->enableHighlightSound.setValue(value.toBool()); +// getSettings()->enableHighlightSound.setValue(value.toBool()); // } // } break; // case 3: { diff --git a/src/debug/Benchmark.cpp b/src/debug/Benchmark.cpp new file mode 100644 index 000000000..2ab28a680 --- /dev/null +++ b/src/debug/Benchmark.cpp @@ -0,0 +1,21 @@ +#include "Benchmark.hpp" + +namespace chatterino { + +BenchmarkGuard::BenchmarkGuard(const QString &_name) + : name_(_name) +{ + timer_.start(); +} + +BenchmarkGuard::~BenchmarkGuard() +{ + log("{} {} ms", this->name_, float(timer_.nsecsElapsed()) / 1000000.0f); +} + +qreal BenchmarkGuard::getElapsedMs() +{ + return qreal(timer_.nsecsElapsed()) / 1000000.0; +} + +} // namespace chatterino diff --git a/src/debug/Benchmark.hpp b/src/debug/Benchmark.hpp index 5eeb6cb36..065048e8f 100644 --- a/src/debug/Benchmark.hpp +++ b/src/debug/Benchmark.hpp @@ -1,42 +1,22 @@ #pragma once -#include +#include "debug/Log.hpp" + #include -#include #include -#define BENCH(x) \ - QElapsedTimer x; \ - x.start(); - -#define MARK(x) \ - qDebug() << BOOST_CURRENT_FUNCTION << __LINE__ \ - << static_cast(x.nsecsElapsed()) / 1000000.0 << "ms"; - namespace chatterino { class BenchmarkGuard : boost::noncopyable { - QElapsedTimer timer; - QString name; - public: - BenchmarkGuard(const QString &_name) - : name(_name) - { - timer.start(); - } + BenchmarkGuard(const QString &_name); + ~BenchmarkGuard(); + qreal getElapsedMs(); - ~BenchmarkGuard() - { - qDebug() << this->name << float(timer.nsecsElapsed()) / 1000000.0f - << "ms"; - } - - qreal getElapsedMs() - { - return qreal(timer.nsecsElapsed()) / 1000000.0; - } +private: + QElapsedTimer timer_; + QString name_; }; } // namespace chatterino diff --git a/src/debug/Log.hpp b/src/debug/Log.hpp index 51ef0f053..31536781a 100644 --- a/src/debug/Log.hpp +++ b/src/debug/Log.hpp @@ -8,17 +8,22 @@ namespace chatterino { template -inline void Log(const std::string &formatString, Args &&... args) +inline void log(const std::string &formatString, Args &&... args) { qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz") << fS(formatString, std::forward(args)...).c_str(); } template -inline void Warn(const std::string &formatString, Args &&... args) +inline void log(const char *formatString, Args &&... args) { - qWarning() << QTime::currentTime().toString("hh:mm:ss.zzz") - << fS(formatString, std::forward(args)...).c_str(); + log(std::string(formatString), std::forward(args)...); +} + +template +inline void log(const QString &formatString, Args &&... args) +{ + log(formatString.toStdString(), std::forward(args)...); } } // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 5f259a8be..4bf6e7daf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,13 +5,15 @@ #include #include - -#include +#include using namespace chatterino; int main(int argc, char **argv) { + auto shared = std::make_shared(); + log(std::atomic_is_lock_free(&shared)); + QApplication a(argc, argv); // convert char** to QStringList diff --git a/src/messages/Emote.cpp b/src/messages/Emote.cpp index 369725426..318e2d300 100644 --- a/src/messages/Emote.cpp +++ b/src/messages/Emote.cpp @@ -15,61 +15,31 @@ bool operator!=(const Emote &a, const Emote &b) return !(a == b); } -// EmotePtr Emote::create(const EmoteData2 &data) -//{ -//} +EmotePtr cachedOrMakeEmotePtr(Emote &&emote, const EmoteMap &cache) +{ + // reuse old shared_ptr if nothing changed + auto it = cache.find(emote.name); + if (it != cache.end() && *it->second == emote) return it->second; -// EmotePtr Emote::create(EmoteData2 &&data) -//{ -//} + return std::make_shared(std::move(emote)); +} -// Emote::Emote(EmoteData2 &&data) -// : data_(data) -//{ -//} -// -// Emote::Emote(const EmoteData2 &data) -// : data_(data) -//{ -//} -// -// const Url &Emote::getHomePage() const -//{ -// return this->data_.homePage; -//} -// -// const EmoteName &Emote::getName() const -//{ -// return this->data_.name; -//} -// -// const Tooltip &Emote::getTooltip() const -//{ -// return this->data_.tooltip; -//} -// -// const ImageSet &Emote::getImages() const -//{ -// return this->data_.images; -//} -// -// const QString &Emote::getCopyString() const -//{ -// return this->data_.name.string; -//} -// -// bool Emote::operator==(const Emote &other) const -//{ -// auto &a = this->data_; -// auto &b = other.data_; -// -// return std::tie(a.homePage, a.name, a.tooltip, a.images) == -// std::tie(b.homePage, b.name, b.tooltip, b.images); -//} -// -// bool Emote::operator!=(const Emote &other) const -//{ -// return !this->operator==(other); -//} +EmotePtr cachedOrMakeEmotePtr( + Emote &&emote, + std::unordered_map> &cache, + std::mutex &mutex, const EmoteId &id) +{ + std::lock_guard guard(mutex); + + auto shared = cache[id].lock(); + if (shared && *shared == emote) { + // reuse old shared_ptr if nothing changed + return shared; + } else { + shared = std::make_shared(std::move(emote)); + cache[id] = shared; + return shared; + } +} } // namespace chatterino diff --git a/src/messages/Emote.hpp b/src/messages/Emote.hpp index c5817752e..143d3ea64 100644 --- a/src/messages/Emote.hpp +++ b/src/messages/Emote.hpp @@ -7,9 +7,6 @@ #include #include -QStringAlias(EmoteId); -QStringAlias(EmoteName); - namespace chatterino { struct Emote { @@ -37,29 +34,10 @@ using EmoteIdMap = std::unordered_map; using WeakEmoteMap = std::unordered_map>; using WeakEmoteIdMap = std::unordered_map>; -// struct EmoteData2 { -// EmoteName name; -// ImageSet images; -// Tooltip tooltip; -// Url homePage; -//}; -// -// class Emote -//{ -// public: -// Emote(EmoteData2 &&data); -// Emote(const EmoteData2 &data); -// -// const Url &getHomePage() const; -// const EmoteName &getName() const; -// const Tooltip &getTooltip() const; -// const ImageSet &getImages() const; -// const QString &getCopyString() const; -// bool operator==(const Emote &other) const; -// bool operator!=(const Emote &other) const; -// -// private: -// EmoteData2 data_; -//}; +EmotePtr cachedOrMakeEmotePtr(Emote &&emote, const EmoteMap &cache); +EmotePtr cachedOrMakeEmotePtr( + Emote &&emote, + std::unordered_map> &cache, + std::mutex &mutex, const EmoteId &id); } // namespace chatterino diff --git a/src/messages/EmoteCache.hpp b/src/messages/EmoteCache.hpp deleted file mode 100644 index 1bbb01fd4..000000000 --- a/src/messages/EmoteCache.hpp +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" - -namespace chatterino { - -template -class MapReplacement -{ -public: - MapReplacement(std::unordered_map &items) - : oldItems_(items) - { - } - - void add(const TKey &key, const Emote &data) - { - this->add(key, Emote(data)); - } - - void add(const TKey &key, Emote &&data) - { - auto it = this->oldItems_.find(key); - if (it != this->oldItems_.end() && *it->second == data) { - this->newItems_[key] = it->second; - } else { - this->newItems_[key] = std::make_shared(std::move(data)); - } - } - - void apply() - { - this->oldItems_ = std::move(this->newItems_); - } - -private: - std::unordered_map &oldItems_; - std::unordered_map newItems_; -}; - -template -class EmoteCache -{ -public: - using Iterator = typename std::unordered_map::iterator; - using ConstIterator = typename std::unordered_map::iterator; - - Iterator begin() - { - return this->items_.begin(); - } - - ConstIterator begin() const - { - return this->items_.begin(); - } - - Iterator end() - { - return this->items_.end(); - } - - ConstIterator end() const - { - return this->items_.end(); - } - - boost::optional get(const TKey &key) const - { - auto it = this->items_.find(key); - - if (it == this->items_.end()) - return boost::none; - else - return it->second; - } - - MapReplacement makeReplacment() - { - return MapReplacement(this->items_); - } - -private: - std::unordered_map items_; -}; - -} // namespace chatterino diff --git a/src/messages/EmoteMap.cpp b/src/messages/EmoteMap.cpp deleted file mode 100644 index f940de852..000000000 --- a/src/messages/EmoteMap.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "EmoteMap.hpp" - -#include "Application.hpp" -#include "singletons/Settings.hpp" - -namespace chatterino { - -// EmoteData::EmoteData(Image *image) -// : image1x(image) -//{ -//} - -//// Emotes must have a 1x image to be valid -// bool EmoteData::isValid() const -//{ -// return this->image1x != nullptr; -//} - -// Image *EmoteData::getImage(float scale) const -//{ -// int quality = getApp()->settings->preferredEmoteQuality; - -// if (quality == 0) { -// scale *= getApp()->settings->emoteScale.getValue(); -// quality = [&] { -// if (scale <= 1) return 1; -// if (scale <= 2) return 2; -// return 3; -// }(); -// } - -// Image *_image; -// if (quality == 3 && this->image3x != nullptr) { -// _image = this->image3x; -// } else if (quality >= 2 && this->image2x != nullptr) { -// _image = this->image2x; -// } else { -// _image = this->image1x; -// } - -// return _image; -//} - -} // namespace chatterino diff --git a/src/messages/EmoteMap.hpp b/src/messages/EmoteMap.hpp deleted file mode 100644 index 30a0dfdd1..000000000 --- a/src/messages/EmoteMap.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "boost/optional.hpp" -#include "messages/Emote.hpp" - -namespace chatterino { - -// class EmoteMap -//{ -// public: -// void add(Emote emote); -// void remove(const Emote &emote); -// void remove(const QString &name); - -// private: -//}; - -// using EmoteMap = ConcurrentMap; - -} // namespace chatterino diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 5935c4a01..e6dcfbc8b 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -1,8 +1,10 @@ #include "messages/Image.hpp" #include "Application.hpp" +#include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "debug/AssertInGuiThread.hpp" +#include "debug/Benchmark.hpp" #include "debug/Log.hpp" #include "singletons/Emotes.hpp" #include "singletons/WindowManager.hpp" @@ -15,119 +17,171 @@ #include #include #include - #include #include namespace chatterino { namespace { -const QPixmap *getPixmap(const Pixmap &pixmap) -{ - if (pixmap.which() == 0) - return boost::get(pixmap); - else - return boost::get>(pixmap).get(); -} + // Frames + Frames::Frames() + { + DebugCount::increase("images"); + } -// Frames -Frames::Frames() -{ - DebugCount::increase("images"); -} + Frames::Frames(const QVector> &frames) + : items_(frames) + { + assertInGuiThread(); + DebugCount::increase("images"); -Frames::Frames(std::vector &&frames) - : items_(std::move(frames)) -{ - DebugCount::increase("images"); - if (this->animated()) DebugCount::increase("animated images"); -} + if (this->animated()) { + DebugCount::increase("animated images"); -Frames::~Frames() -{ - DebugCount::decrease("images"); - if (this->animated()) DebugCount::decrease("animated images"); -} - -void Frames::advance() -{ - this->durationOffset_ += GIF_FRAME_LENGTH; - - while (true) { - this->index_ %= this->items_.size(); - if (this->durationOffset_ > this->items_[this->index_].duration) { - this->durationOffset_ -= this->items_[this->index_].duration; - this->index_ = (this->index_ + 1) % this->items_.size(); - } else { - break; + this->gifTimerConnection_ = + getApp()->emotes->gifTimer.signal.connect( + [this] { this->advance(); }); } } -} -bool Frames::animated() const -{ - return this->items_.size() > 1; -} + Frames::~Frames() + { + assertInGuiThread(); + DebugCount::decrease("images"); -const QPixmap *Frames::current() const -{ - if (this->items_.size() == 0) return nullptr; - return getPixmap(this->items_[this->index_].pixmap); -} + if (this->animated()) { + DebugCount::decrease("animated images"); + } -const QPixmap *Frames::first() const -{ - if (this->items_.size() == 0) return nullptr; - return getPixmap(this->items_.front().pixmap); -} + this->gifTimerConnection_.disconnect(); + } -// functions -std::vector readFrames(QImageReader &reader, const Url &url) -{ - std::vector frames; + void Frames::advance() + { + this->durationOffset_ += GIF_FRAME_LENGTH; + + while (true) { + this->index_ %= this->items_.size(); + + if (this->index_ >= this->items_.size()) { + this->index_ = this->index_; + } + + if (this->durationOffset_ > this->items_[this->index_].duration) { + this->durationOffset_ -= this->items_[this->index_].duration; + this->index_ = (this->index_ + 1) % this->items_.size(); + } else { + break; + } + } + } + + bool Frames::animated() const + { + return this->items_.size() > 1; + } + + boost::optional Frames::current() const + { + if (this->items_.size() == 0) return boost::none; + return this->items_[this->index_].image; + } + + boost::optional Frames::first() const + { + if (this->items_.size() == 0) return boost::none; + return this->items_.front().image; + } + + // functions + QVector> readFrames(QImageReader &reader, const Url &url) + { + QVector> frames; + + if (reader.imageCount() == 0) { + log("Error while reading image {}: '{}'", url.string, + reader.errorString()); + return frames; + } + + QImage image; + for (int index = 0; index < reader.imageCount(); ++index) { + if (reader.read(&image)) { + QPixmap::fromImage(image); + + int duration = std::max(20, reader.nextImageDelay()); + frames.push_back(Frame{image, duration}); + } + } + + if (frames.size() == 0) { + log("Error while reading image {}: '{}'", url.string, + reader.errorString()); + } - if (reader.imageCount() <= 0) { - Log("Error while reading image {}: '{}'", url.string, - reader.errorString()); return frames; } - QImage image; - for (int index = 0; index < reader.imageCount(); ++index) { - if (reader.read(&image)) { - auto pixmap = std::make_unique(QPixmap::fromImage(image)); + // parsed + template + void assignDelayed( + std::queue>>> &queued, + std::mutex &mutex, std::atomic_bool &loadedEventQueued) + { + std::lock_guard lock(mutex); + int i = 0; - int duration = std::max(20, reader.nextImageDelay()); - frames.push_back(Frame{std::move(pixmap), duration}); + while (!queued.empty()) { + queued.front().first(queued.front().second); + queued.pop(); + + if (++i > 50) { + QTimer::singleShot(3, [&] { + assignDelayed(queued, mutex, loadedEventQueued); + }); + return; + } } + + getApp()->windows->forceLayoutChannelViews(); + loadedEventQueued = false; } - if (frames.size() != 0) { - Log("Error while reading image {}: '{}'", url.string, - reader.errorString()); + template + auto makeConvertCallback(const QVector> &parsed, + Assign assign) + { + return [parsed, assign] { + // convert to pixmap + auto frames = QVector>(); + std::transform(parsed.begin(), parsed.end(), + std::back_inserter(frames), [](auto &frame) { + return Frame{ + QPixmap::fromImage(frame.image), + frame.duration}; + }); + + // put into stack + static std::queue>>> + queued; + static std::mutex mutex; + + std::lock_guard lock(mutex); + queued.emplace(assign, frames); + + static std::atomic_bool loadedEventQueued{false}; + + if (!loadedEventQueued) { + loadedEventQueued = true; + + QTimer::singleShot(100, [=] { + assignDelayed(queued, mutex, loadedEventQueued); + }); + } + }; } - - return frames; -} - -void queueLoadedEvent() -{ - static auto eventQueued = false; - - if (!eventQueued) { - eventQueued = true; - - QTimer::singleShot(250, [] { - getApp()->windows->incGeneration(); - getApp()->windows->layoutChannelViews(); - eventQueued = false; - }); - } -} } // namespace // IMAGE2 -std::atomic Image::loadedEventQueued{false}; - ImagePtr Image::fromUrl(const Url &url, qreal scale) { static std::unordered_map> cache; @@ -140,18 +194,13 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale) if (!shared) { cache[url] = shared = ImagePtr(new Image(url, scale)); } else { - Warn("same image loaded multiple times: {}", url.string); + // Warn("same image loaded multiple times: {}", url.string); } return shared; } -ImagePtr Image::fromOwningPixmap(std::unique_ptr pixmap, qreal scale) -{ - return ImagePtr(new Image(std::move(pixmap), scale)); -} - -ImagePtr Image::fromNonOwningPixmap(QPixmap *pixmap, qreal scale) +ImagePtr Image::fromPixmap(const QPixmap &pixmap, qreal scale) { return ImagePtr(new Image(pixmap, scale)); } @@ -171,23 +220,15 @@ Image::Image(const Url &url, qreal scale) : url_(url) , scale_(scale) , shouldLoad_(true) + , frames_(std::make_unique()) { } -Image::Image(std::unique_ptr owning, qreal scale) +Image::Image(const QPixmap &pixmap, qreal scale) : scale_(scale) + , frames_(std::make_unique( + QVector>{Frame{pixmap, 1}})) { - std::vector vec; - vec.push_back(Frame{std::move(owning)}); - this->frames_ = std::move(vec); -} - -Image::Image(QPixmap *nonOwning, qreal scale) - : scale_(scale) -{ - std::vector vec; - vec.push_back(Frame{nonOwning}); - this->frames_ = std::move(vec); } const Url &Image::url() const @@ -195,7 +236,7 @@ const Url &Image::url() const return this->url_; } -const QPixmap *Image::pixmap() const +boost::optional Image::pixmap() const { assertInGuiThread(); @@ -204,7 +245,7 @@ const QPixmap *Image::pixmap() const const_cast(this)->load(); } - return this->frames_.current(); + return this->frames_->current(); } qreal Image::scale() const @@ -212,7 +253,7 @@ qreal Image::scale() const return this->scale_; } -bool Image::empty() const +bool Image::isEmpty() const { return this->empty_; } @@ -221,14 +262,14 @@ bool Image::animated() const { assertInGuiThread(); - return this->frames_.animated(); + return this->frames_->animated(); } int Image::width() const { assertInGuiThread(); - if (auto pixmap = this->frames_.first()) + if (auto pixmap = this->frames_->first()) return pixmap->width() * this->scale_; else return 16; @@ -238,7 +279,7 @@ int Image::height() const { assertInGuiThread(); - if (auto pixmap = this->frames_.first()) + if (auto pixmap = this->frames_->first()) return pixmap->height() * this->scale_; else return 16; @@ -247,39 +288,37 @@ int Image::height() const void Image::load() { NetworkRequest req(this->url().string); + req.setExecuteConcurrently(true); req.setCaller(&this->object_); req.setUseQuickLoadCache(true); - req.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { - assertInGuiThread(); - + req.onSuccess([that = this, weak = weakOf(this)](auto result) -> Outcome { auto shared = weak.lock(); if (!shared) return Failure; + auto data = result.getData(); + // const cast since we are only reading from it - QBuffer buffer(const_cast(&result.getData())); + QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); QImageReader reader(&buffer); + auto parsed = readFrames(reader, that->url()); + + postToThread(makeConvertCallback(parsed, [weak](auto frames) { + if (auto shared = weak.lock()) + shared->frames_ = std::make_unique(frames); + })); - this->frames_ = readFrames(reader, this->url()); return Success; }); - req.onError([this, weak = weakOf(this)](int) { - auto shared = weak.lock(); - if (!shared) return false; - - this->frames_ = std::vector(); - - return false; - }); req.execute(); } bool Image::operator==(const Image &other) const { - if (this->empty() && other.empty()) return true; + if (this->isEmpty() && other.isEmpty()) return true; if (!this->url_.string.isEmpty() && this->url_ == other.url_) return true; - if (this->frames_.first() == other.frames_.first()) return true; + if (this->frames_->first() == other.frames_->first()) return true; return false; } diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 50af47f4e..678af25ce 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -1,43 +1,45 @@ #pragma once -#include "common/Common.hpp" - #include #include +#include +#include #include #include +#include #include #include #include +#include +#include "common/Aliases.hpp" #include "common/NullablePtr.hpp" namespace chatterino { namespace { -using Pixmap = boost::variant>; -struct Frame { - Pixmap pixmap; - int duration; -}; -class Frames -{ -public: - Frames(); - Frames(std::vector &&frames); - ~Frames(); - Frames(Frames &&other) = default; - Frames &operator=(Frames &&other) = default; + template + struct Frame { + Image image; + int duration; + }; + class Frames : boost::noncopyable + { + public: + Frames(); + Frames(const QVector> &frames); + ~Frames(); - bool animated() const; - void advance(); - const QPixmap *current() const; - const QPixmap *first() const; + bool animated() const; + void advance(); + boost::optional current() const; + boost::optional first() const; -private: - std::vector items_; - int index_{0}; - int durationOffset_{0}; -}; + private: + QVector> items_; + int index_{0}; + int durationOffset_{0}; + pajlada::Signals::Connection gifTimerConnection_; + }; } // namespace class Image; @@ -47,15 +49,13 @@ class Image : public std::enable_shared_from_this, boost::noncopyable { public: static ImagePtr fromUrl(const Url &url, qreal scale = 1); - static ImagePtr fromOwningPixmap(std::unique_ptr pixmap, - qreal scale = 1); - static ImagePtr fromNonOwningPixmap(QPixmap *pixmap, qreal scale = 1); + static ImagePtr fromPixmap(const QPixmap &pixmap, qreal scale = 1); static ImagePtr getEmpty(); const Url &url() const; - const QPixmap *pixmap() const; + boost::optional pixmap() const; qreal scale() const; - bool empty() const; + bool isEmpty() const; int width() const; int height() const; bool animated() const; @@ -66,8 +66,7 @@ public: private: Image(); Image(const Url &url, qreal scale); - Image(std::unique_ptr owning, qreal scale); - Image(QPixmap *nonOwning, qreal scale); + Image(const QPixmap &nonOwning, qreal scale); void load(); @@ -75,9 +74,7 @@ private: qreal scale_{1}; bool empty_{false}; bool shouldLoad_{false}; - Frames frames_{}; + std::unique_ptr frames_{}; QObject object_{}; - - static std::atomic loadedEventQueued; }; } // namespace chatterino diff --git a/src/messages/ImageSet.cpp b/src/messages/ImageSet.cpp index af3c5e6bd..dd4f23b97 100644 --- a/src/messages/ImageSet.cpp +++ b/src/messages/ImageSet.cpp @@ -1,7 +1,5 @@ -#include "ImageSet.hpp" +#include "messages/ImageSet.hpp" -#include "Application.hpp" -#include "singletons/Resources.hpp" #include "singletons/Settings.hpp" namespace chatterino { @@ -60,23 +58,19 @@ const ImagePtr &ImageSet::getImage3() const const ImagePtr &ImageSet::getImage(float scale) const { - int quality = getSettings()->preferredEmoteQuality; + int quality = 1; - if (!quality) { - if (scale > 3.999) - quality = 3; - else if (scale > 1.999) - quality = 2; - else - scale = 1; - } + if (scale > 2.999) + quality = 3; + else if (scale > 1.5) + quality = 2; - if (!this->imageX3_->empty() && quality == 3) { + if (!this->imageX3_->isEmpty() && quality == 3) { return this->imageX3_; } - if (!this->imageX2_->empty() && quality == 2) { - return this->imageX3_; + if (!this->imageX2_->isEmpty() && quality == 2) { + return this->imageX2_; } return this->imageX1_; diff --git a/src/messages/ImageSet.hpp b/src/messages/ImageSet.hpp index 8f2efab2d..e8872b0b7 100644 --- a/src/messages/ImageSet.hpp +++ b/src/messages/ImageSet.hpp @@ -21,8 +21,6 @@ public: const ImagePtr &getImage(float scale) const; - ImagePtr getImage(float scale); - bool operator==(const ImageSet &other) const; bool operator!=(const ImageSet &other) const; diff --git a/src/messages/Link.hpp b/src/messages/Link.hpp index a6c503540..38000982f 100644 --- a/src/messages/Link.hpp +++ b/src/messages/Link.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include namespace chatterino { @@ -14,6 +13,7 @@ public: UserInfo, UserTimeout, UserBan, + UserWhisper, InsertText, ShowMessage, UserAction, diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 06b995b5a..d6a79fcdd 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,16 +1,16 @@ #pragma once #include "common/FlagsEnum.hpp" -#include "messages/MessageElement.hpp" -#include "providers/twitch/PubsubActions.hpp" #include "widgets/helper/ScrollbarHighlight.hpp" #include +#include #include #include #include namespace chatterino { +class MessageElement; enum class MessageFlag : uint16_t { None = 0, diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 42c537c67..49c96c946 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -1,6 +1,9 @@ #include "MessageBuilder.hpp" #include "common/LinkParser.hpp" +#include "messages/Message.hpp" +#include "messages/MessageElement.hpp" +#include "providers/twitch/PubsubActions.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Theme.hpp" @@ -17,7 +20,7 @@ MessagePtr makeSystemMessage(const QString &text) } MessageBuilder::MessageBuilder() - : message_(std::make_unique()) + : message_(std::make_shared()) { } @@ -82,6 +85,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, } MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) + : MessageBuilder() { this->emplace(); this->message().flags.set(MessageFlag::System); @@ -127,6 +131,7 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) } MessageBuilder::MessageBuilder(const UnbanAction &action) + : MessageBuilder() { this->emplace(); this->message().flags.set(MessageFlag::System); @@ -163,7 +168,9 @@ Message &MessageBuilder::message() MessagePtr MessageBuilder::release() { - return MessagePtr(this->message_.release()); + std::shared_ptr ptr; + this->message_.swap(ptr); + return ptr; } void MessageBuilder::append(std::unique_ptr element) diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 5ae197254..d3cbdb8d0 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -1,11 +1,15 @@ #pragma once -#include "messages/Message.hpp" +#include "messages/MessageElement.hpp" #include #include namespace chatterino { +struct BanAction; +struct UnbanAction; +struct Message; +using MessagePtr = std::shared_ptr; struct SystemMessageTag { }; @@ -16,6 +20,14 @@ const TimeoutMessageTag timeoutMessage{}; MessagePtr makeSystemMessage(const QString &text); +struct MessageParseArgs { + bool disablePingSounds = false; + bool isReceivedWhisper = false; + bool isSentWhisper = false; + bool trimSubscriberUsername = false; + bool isStaffOrBroadcaster = false; +}; + class MessageBuilder { public: @@ -48,7 +60,7 @@ public: } private: - std::unique_ptr message_; + std::shared_ptr message_; }; } // namespace chatterino diff --git a/src/messages/MessageColor.cpp b/src/messages/MessageColor.cpp index f22654b51..fe793b30e 100644 --- a/src/messages/MessageColor.cpp +++ b/src/messages/MessageColor.cpp @@ -1,5 +1,7 @@ #include "MessageColor.hpp" +#include "singletons/Theme.hpp" + namespace chatterino { MessageColor::MessageColor(const QColor &color) diff --git a/src/messages/MessageColor.hpp b/src/messages/MessageColor.hpp index e7d828745..dd19692c9 100644 --- a/src/messages/MessageColor.hpp +++ b/src/messages/MessageColor.hpp @@ -1,10 +1,9 @@ #pragma once -#include "singletons/Theme.hpp" - #include namespace chatterino { +class Theme; struct MessageColor { enum Type { Custom, Text, Link, System }; diff --git a/src/messages/MessageContainer.cpp b/src/messages/MessageContainer.cpp new file mode 100644 index 000000000..2d47cb906 --- /dev/null +++ b/src/messages/MessageContainer.cpp @@ -0,0 +1,9 @@ +#include "MessageContainer.hpp" + +namespace chatterino { + +MessageContainer::MessageContainer() +{ +} + +} // namespace chatterino diff --git a/src/messages/MessageContainer.hpp b/src/messages/MessageContainer.hpp new file mode 100644 index 000000000..781135e60 --- /dev/null +++ b/src/messages/MessageContainer.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace chatterino { + +class MessageContainer +{ +public: + MessageContainer(); +}; + +} // namespace chatterino diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index bca2c3072..b9271d2ed 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,12 +1,13 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" -#include "common/Emotemap.hpp" #include "controllers/moderationactions/ModerationActions.hpp" #include "debug/Benchmark.hpp" +#include "messages/Emote.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" #include "messages/layouts/MessageLayoutElement.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "util/DebugCount.hpp" namespace chatterino { @@ -102,7 +103,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { if (flags.has(MessageElementFlag::EmoteImages)) { auto image = this->emote_->images.getImage(container.getScale()); - if (image->empty()) return; + if (image->isEmpty()) return; auto size = QSize(int(container.getScale() * image->width()), int(container.getScale() * image->height())); @@ -224,8 +225,8 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container, { if (flags.hasAny(this->getFlags())) { auto app = getApp(); - if (app->settings->timestampFormat != this->format_) { - this->format_ = app->settings->timestampFormat.getValue(); + if (getSettings()->timestampFormat != this->format_) { + this->format_ = getSettings()->timestampFormat.getValue(); this->element_.reset(this->formatTime(this->time_)); } @@ -237,7 +238,7 @@ TextElement *TimestampElement::formatTime(const QTime &time) { static QLocale locale("en_US"); - QString format = locale.toString(time, getApp()->settings->timestampFormat); + QString format = locale.toString(time, getSettings()->timestampFormat); return new TextElement(format, MessageElementFlag::Timestamp, MessageColor::System, FontStyle::ChatMedium); diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 82edadd85..8d96d1135 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -1,9 +1,6 @@ #pragma once -#include "common/Emotemap.hpp" #include "common/FlagsEnum.hpp" -#include "messages/Emote.hpp" -#include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" #include "singletons/Fonts.hpp" @@ -12,14 +9,20 @@ #include #include #include - #include #include +#include namespace chatterino { class Channel; struct MessageLayoutContainer; +class Image; +using ImagePtr = std::shared_ptr; + +struct Emote; +using EmotePtr = std::shared_ptr; + enum class MessageElementFlag { None = 0, Misc = (1 << 0), diff --git a/src/messages/MessageParseArgs.hpp b/src/messages/MessageParseArgs.hpp index f3d732c4b..471d1d648 100644 --- a/src/messages/MessageParseArgs.hpp +++ b/src/messages/MessageParseArgs.hpp @@ -2,12 +2,4 @@ namespace chatterino { -struct MessageParseArgs { - bool disablePingSounds = false; - bool isReceivedWhisper = false; - bool isSentWhisper = false; - bool trimSubscriberUsername = false; - bool isStaffOrBroadcaster = false; -}; - } // namespace chatterino diff --git a/src/messages/Selection.hpp b/src/messages/Selection.hpp index dbed15d30..7cffebbec 100644 --- a/src/messages/Selection.hpp +++ b/src/messages/Selection.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -23,14 +24,8 @@ struct SelectionItem { bool operator<(const SelectionItem &b) const { - if (this->messageIndex < b.messageIndex) { - return true; - } - if (this->messageIndex == b.messageIndex && - this->charIndex < b.charIndex) { - return true; - } - return false; + return std::tie(this->messageIndex, this->charIndex) < + std::tie(b.messageIndex, b.charIndex); } bool operator>(const SelectionItem &b) const diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 9847c1950..5fbbe0bfa 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -2,8 +2,12 @@ #include "Application.hpp" #include "debug/Benchmark.hpp" +#include "messages/Message.hpp" +#include "messages/MessageElement.hpp" +#include "messages/layouts/MessageLayoutContainer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/DebugCount.hpp" @@ -24,6 +28,7 @@ namespace chatterino { MessageLayout::MessageLayout(MessagePtr message) : message_(message) , buffer_(nullptr) + , container_(std::make_shared()) { DebugCount::increase("message layout"); } @@ -41,7 +46,7 @@ const Message *MessageLayout::getMessage() // Height int MessageLayout::getHeight() const { - return container_.getHeight(); + return container_->getHeight(); } // Layout @@ -68,7 +73,7 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) // check if work mask changed layoutRequired |= this->currentWordFlags_ != flags; - this->currentWordFlags_ = flags; // app->settings->getWordTypeMask(); + this->currentWordFlags_ = flags; // getSettings()->getWordTypeMask(); // check if layout was requested manually layoutRequired |= this->flags.has(MessageLayoutFlag::RequiresLayout); @@ -82,9 +87,9 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) return false; } - int oldHeight = this->container_.getHeight(); + int oldHeight = this->container_->getHeight(); this->actuallyLayout(width, flags); - if (widthChanged || this->container_.getHeight() != oldHeight) { + if (widthChanged || this->container_->getHeight() != oldHeight) { this->deleteBuffer(); } this->invalidateBuffer(); @@ -103,22 +108,22 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags) messageFlags.unset(MessageFlag::Collapsed); } - this->container_.begin(width, this->scale_, messageFlags); + this->container_->begin(width, this->scale_, messageFlags); for (const auto &element : this->message_->elements) { - element->addToContainer(this->container_, _flags); + element->addToContainer(*this->container_, _flags); } - if (this->height_ != this->container_.getHeight()) { + if (this->height_ != this->container_->getHeight()) { this->deleteBuffer(); } - this->container_.end(); - this->height_ = this->container_.getHeight(); + this->container_->end(); + this->height_ = this->container_->getHeight(); // collapsed state this->flags.unset(MessageLayoutFlag::Collapsed); - if (this->container_.isCollapsed()) { + if (this->container_->isCollapsed()) { this->flags.set(MessageLayoutFlag::Collapsed); } } @@ -135,11 +140,12 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, if (!pixmap) { #ifdef Q_OS_MACOS pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()), - int(container_.getHeight() * + int(container_->getHeight() * painter.device()->devicePixelRatioF())); pixmap->setDevicePixelRatio(painter.device()->devicePixelRatioF()); #else - pixmap = new QPixmap(width, std::max(16, this->container_.getHeight())); + pixmap = + new QPixmap(width, std::max(16, this->container_->getHeight())); #endif this->buffer_ = std::shared_ptr(pixmap); @@ -157,7 +163,7 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, // this->container.getHeight(), *pixmap); // draw gif emotes - this->container_.paintAnimatedElements(painter, y); + this->container_->paintAnimatedElements(painter, y); // draw disabled if (this->message_->flags.has(MessageFlag::Disabled)) { @@ -167,12 +173,12 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, // draw selection if (!selection.isEmpty()) { - this->container_.paintSelection(painter, messageIndex, selection, y); + this->container_->paintSelection(painter, messageIndex, selection, y); } // draw message seperation line - if (app->settings->separateMessages.getValue()) { - painter.fillRect(0, y, this->container_.getWidth(), 1, + if (getSettings()->separateMessages.getValue()) { + painter.fillRect(0, y, this->container_->getWidth(), 1, app->themes->splits.messageSeperator); } @@ -184,9 +190,9 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, : app->themes->tabs.selected.backgrounds.unfocused.color(); QBrush brush(color, static_cast( - app->settings->lastMessagePattern.getValue())); + getSettings()->lastMessagePattern.getValue())); - painter.fillRect(0, y + this->container_.getHeight() - 1, + painter.fillRect(0, y + this->container_->getHeight() - 1, pixmap->width(), 1, brush); } @@ -208,7 +214,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, backgroundColor = app->themes->messages.backgrounds.highlighted; } else if (this->message_->flags.has(MessageFlag::Subscription)) { backgroundColor = app->themes->messages.backgrounds.subscription; - } else if (app->settings->alternateMessageBackground.getValue() && + } else if (getSettings()->alternateMessageBackground.getValue() && this->flags.has(MessageLayoutFlag::AlternateBackground)) { backgroundColor = app->themes->messages.backgrounds.alternate; } else { @@ -217,7 +223,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, painter.fillRect(buffer->rect(), backgroundColor); // draw message - this->container_.paintElements(painter); + this->container_->paintElements(painter); #ifdef FOURTF // debug @@ -252,7 +258,7 @@ void MessageLayout::deleteCache() this->deleteBuffer(); #ifdef XD - this->container_.clear(); + this->container_->clear(); #endif } @@ -265,22 +271,23 @@ void MessageLayout::deleteCache() const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) { // go through all words and return the first one that contains the point. - return this->container_.getElementAt(point); + return this->container_->getElementAt(point); } int MessageLayout::getLastCharacterIndex() const { - return this->container_.getLastCharacterIndex(); + return this->container_->getLastCharacterIndex(); } int MessageLayout::getSelectionIndex(QPoint position) { - return this->container_.getSelectionIndex(position); + return this->container_->getSelectionIndex(position); } -void MessageLayout::addSelectionText(QString &str, int from, int to) +void MessageLayout::addSelectionText(QString &str, int from, int to, + CopyMode copymode) { - this->container_.addSelectionText(str, from, to); + this->container_->addSelectionText(str, from, to, copymode); } } // namespace chatterino diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index 629da9685..671d30668 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -1,19 +1,25 @@ #pragma once +#include "common/Common.hpp" #include "common/FlagsEnum.hpp" -#include "messages/Message.hpp" -#include "messages/Selection.hpp" -#include "messages/layouts/MessageLayoutContainer.hpp" -#include "messages/layouts/MessageLayoutElement.hpp" #include - #include #include #include namespace chatterino { +struct Message; +using MessagePtr = std::shared_ptr; + +struct Selection; +struct MessageLayoutContainer; +class MessageLayoutElement; + +enum class MessageElementFlag; +using MessageElementFlags = FlagsEnum; + enum class MessageLayoutFlag : uint8_t { RequiresBufferUpdate = 1 << 1, RequiresLayout = 1 << 2, @@ -31,13 +37,10 @@ public: const Message *getMessage(); - // Height int getHeight() const; - // Flags MessageLayoutFlags flags; - // Layout bool layout(int width, float scale_, MessageElementFlags flags); // Painting @@ -52,7 +55,8 @@ public: const MessageLayoutElement *getElementAt(QPoint point); int getLastCharacterIndex() const; int getSelectionIndex(QPoint position); - void addSelectionText(QString &str, int from = 0, int to = INT_MAX); + void addSelectionText(QString &str, int from = 0, int to = INT_MAX, + CopyMode copymode = CopyMode::Everything); // Misc bool isDisabled() const; @@ -60,7 +64,7 @@ public: private: // variables MessagePtr message_; - MessageLayoutContainer container_; + std::shared_ptr container_; std::shared_ptr buffer_ = nullptr; bool bufferValid_ = false; diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 5e6350433..0aef73c6b 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -1,16 +1,20 @@ #include "MessageLayoutContainer.hpp" #include "Application.hpp" -#include "MessageLayoutElement.hpp" +#include "messages/Message.hpp" +#include "messages/MessageElement.hpp" #include "messages/Selection.hpp" +#include "messages/layouts/MessageLayoutElement.hpp" +#include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include #include #define COMPACT_EMOTES_OFFSET 6 #define MAX_UNCOLLAPSED_LINES \ - (getApp()->settings->collpseMessagesMinLines.getValue()) + (getSettings()->collpseMessagesMinLines.getValue()) namespace chatterino { @@ -126,9 +130,10 @@ void MessageLayoutContainer::breakLine() int xOffset = 0; if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0) { - xOffset = (width_ - this->elements_.at(this->elements_.size() - 1) - ->getRect() - .right()) / + xOffset = (width_ - this->elements_.at(0)->getRect().left() - + this->elements_.at(this->elements_.size() - 1) + ->getRect() + .right()) / 2; } @@ -230,7 +235,7 @@ void MessageLayoutContainer::end() bool MessageLayoutContainer::canCollapse() { - return getApp()->settings->collpseMessagesMinLines.getValue() > 0 && + return getSettings()->collpseMessagesMinLines.getValue() > 0 && this->flags_.has(MessageFlag::Collapsed); } @@ -500,33 +505,41 @@ int MessageLayoutContainer::getLastCharacterIndex() const return this->lines_.back().endCharIndex; } -void MessageLayoutContainer::addSelectionText(QString &str, int from, int to) +void MessageLayoutContainer::addSelectionText(QString &str, int from, int to, + CopyMode copymode) { int index = 0; bool first = true; - for (std::unique_ptr &ele : this->elements_) { - int c = ele->getSelectionIndexCount(); + for (auto &element : this->elements_) { + if (copymode == CopyMode::OnlyTextAndEmotes) { + if (element->getCreator().getFlags().hasAny( + {MessageElementFlag::Timestamp, + MessageElementFlag::Username, MessageElementFlag::Badges})) + continue; + } + + auto indexCount = element->getSelectionIndexCount(); if (first) { - if (index + c > from) { - ele->addCopyTextToString(str, from - index, to - index); + if (index + indexCount > from) { + element->addCopyTextToString(str, from - index, to - index); first = false; - if (index + c > to) { + if (index + indexCount > to) { break; } } } else { - if (index + c > to) { - ele->addCopyTextToString(str, 0, to - index); + if (index + indexCount > to) { + element->addCopyTextToString(str, 0, to - index); break; } else { - ele->addCopyTextToString(str); + element->addCopyTextToString(str); } } - index += c; + index += indexCount; } } diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index f52f181e6..669986196 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -1,18 +1,21 @@ #pragma once -#include -#include - #include #include +#include +#include -#include "messages/Message.hpp" +#include "common/Common.hpp" +#include "common/FlagsEnum.hpp" #include "messages/Selection.hpp" +#include "messages/layouts/MessageLayoutElement.hpp" class QPainter; namespace chatterino { -class MessageLayoutElement; + +enum class MessageFlag : uint16_t; +using MessageFlags = FlagsEnum; struct Margin { int top; @@ -72,7 +75,7 @@ struct MessageLayoutContainer { // selection int getSelectionIndex(QPoint point); int getLastCharacterIndex() const; - void addSelectionText(QString &str, int from, int to); + void addSelectionText(QString &str, int from, int to, CopyMode copymode); bool isCollapsed(); @@ -92,7 +95,7 @@ private: // variables float scale_ = 1.f; int width_ = 0; - MessageFlags flags_ = MessageFlag::None; + MessageFlags flags_{}; int line_ = 0; int height_ = 0; int currentX_ = 0; diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index e8d333e2b..8a683a391 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -1,7 +1,10 @@ #include "messages/layouts/MessageLayoutElement.hpp" #include "Application.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" #include "messages/MessageElement.hpp" +#include "singletons/Theme.hpp" #include "util/DebugCount.hpp" #include @@ -75,11 +78,12 @@ ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, void ImageLayoutElement::addCopyTextToString(QString &str, int from, int to) const { - // str += this->image_->getCopyString(); - str += "not implemented"; - - if (this->hasTrailingSpace()) { - str += " "; + const auto *emoteElement = dynamic_cast(&this->getCreator()); + if (emoteElement) { + str += emoteElement->getEmote()->getCopyString(); + if (this->hasTrailingSpace()) { + str += " "; + } } } diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 44b4c9316..7167adaa9 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -3,19 +3,19 @@ #include #include #include - #include #include -#include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" -#include "singletons/Fonts.hpp" class QPainter; namespace chatterino { class MessageElement; +class Image; +using ImagePtr = std::shared_ptr; +enum class FontStyle : uint8_t; class MessageLayoutElement : boost::noncopyable { diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index b9ca7ba99..2efe00d64 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -1,7 +1,9 @@ #include "providers/bttv/BttvEmotes.hpp" +#include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/ImageSet.hpp" #include "providers/twitch/TwitchChannel.hpp" @@ -10,11 +12,140 @@ #include namespace chatterino { - namespace { + Url getEmoteLink(QString urlTemplate, const EmoteId &id, + const QString &emoteScale) + { + urlTemplate.detach(); -Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) + return {urlTemplate.replace("{{id}}", id.string) + .replace("{{image}}", emoteScale)}; + } + std::pair parseGlobalEmotes( + const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes) + { + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = + qS("https:") + jsonRoot.value("urlTemplate").toString(); + + for (auto jsonEmote : jsonEmotes) { + auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; + auto name = + EmoteName{jsonEmote.toObject().value("code").toString()}; + + auto emote = Emote( + {name, + ImageSet{ + Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Global Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); + + emotes[name] = + cachedOrMakeEmotePtr(std::move(emote), currentEmotes); + } + + return {Success, std::move(emotes)}; + } + EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) + { + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); + } + std::pair parseChannelEmotes(const QJsonObject &jsonRoot) + { + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = "https:" + jsonRoot.value("urlTemplate").toString(); + + for (auto jsonEmote_ : jsonEmotes) { + auto jsonEmote = jsonEmote_.toObject(); + + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto name = EmoteName{jsonEmote.value("code").toString()}; + // emoteObject.value("imageType").toString(); + + auto emote = Emote( + {name, + ImageSet{ + Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Channel Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); + + emotes[name] = cachedOrMake(std::move(emote), id); + } + + return {Success, std::move(emotes)}; + } +} // namespace + +// +// BttvEmotes +// +BttvEmotes::BttvEmotes() + : global_(std::make_shared()) +{ +} + +std::shared_ptr BttvEmotes::emotes() const +{ + return this->global_.get(); +} + +boost::optional BttvEmotes::emote(const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +void BttvEmotes::loadEmotes() +{ + auto request = NetworkRequest(QString(globalEmoteApiUrl)); + + request.setCaller(QThread::currentThread()); + request.setTimeout(30000); + + request.onSuccess([this](auto result) -> Outcome { + auto emotes = this->global_.get(); + auto pair = parseGlobalEmotes(result.parseJson(), *emotes); + if (pair.first) + this->global_.set( + std::make_shared(std::move(pair.second))); + return pair.first; + }); + + request.execute(); +} + +void BttvEmotes::loadChannel(const QString &channelName, + std::function callback) +{ + auto request = + NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelName); + + request.setCaller(QThread::currentThread()); + request.setTimeout(3000); + + request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { + auto pair = parseChannelEmotes(result.parseJson()); + if (pair.first) callback(std::move(pair.second)); + return pair.first; + }); + + request.execute(); +} + +static Url getEmoteLink(QString urlTemplate, const EmoteId &id, + const QString &emoteScale) { urlTemplate.detach(); @@ -22,93 +153,4 @@ Url getEmoteLink(QString urlTemplate, const EmoteId &id, .replace("{{image}}", emoteScale)}; } -} // namespace - -AccessGuard BttvEmotes::accessGlobalEmotes() const -{ - return this->globalEmotes_.accessConst(); -} - -boost::optional BttvEmotes::getGlobalEmote(const EmoteName &name) -{ - auto emotes = this->globalEmotes_.access(); - auto it = emotes->find(name); - - if (it == emotes->end()) return boost::none; - return it->second; -} - -// FOURTF: never returns anything -// boost::optional BttvEmotes::getEmote(const EmoteId &id) -//{ -// auto cache = this->channelEmoteCache_.access(); -// auto it = cache->find(id); -// -// if (it != cache->end()) { -// auto shared = it->second.lock(); -// if (shared) { -// return shared; -// } -// } -// -// return boost::none; -//} - -void BttvEmotes::loadGlobalEmotes() -{ - auto request = NetworkRequest(QString(globalEmoteApiUrl)); - - request.setCaller(QThread::currentThread()); - request.setTimeout(30000); - request.onSuccess([this](auto result) -> Outcome { - // if (auto shared = weak.lock()) { - auto currentEmotes = this->globalEmotes_.access(); - - auto pair = this->parseGlobalEmotes(result.parseJson(), *currentEmotes); - - if (pair.first) { - *currentEmotes = std::move(pair.second); - } - - return pair.first; - // } - return Failure; - }); - - request.execute(); -} - -std::pair BttvEmotes::parseGlobalEmotes( - const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes) -{ - auto emotes = EmoteMap(); - auto jsonEmotes = jsonRoot.value("emotes").toArray(); - auto urlTemplate = - QString("https:" + jsonRoot.value("urlTemplate").toString()); - - for (const QJsonValue &jsonEmote : jsonEmotes) { - auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; - auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; - - auto emote = Emote( - {name, - ImageSet{ - Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), - Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), - Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, - Tooltip{name.string + "
Global Bttv Emote"}, - Url{"https://manage.betterttv.net/emotes/" + id.string}}); - - auto it = currentEmotes.find(name); - if (it != currentEmotes.end() && *it->second == emote) { - // reuse old shared_ptr if nothing changed - emotes[name] = it->second; - } else { - emotes[name] = std::make_shared(std::move(emote)); - } - } - - return {Success, std::move(emotes)}; -} - } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index f85333df7..c82cb67a4 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -1,33 +1,34 @@ #pragma once #include - -#include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" -#include "messages/EmoteCache.hpp" +#include "boost/optional.hpp" +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" namespace chatterino { -class BttvEmotes final : std::enable_shared_from_this +struct Emote; +using EmotePtr = std::shared_ptr; +class EmoteMap; + +class BttvEmotes final { static constexpr const char *globalEmoteApiUrl = "https://api.betterttv.net/2/emotes"; + static constexpr const char *bttvChannelEmoteApiUrl = + "https://api.betterttv.net/2/channels/"; public: - // BttvEmotes(); + BttvEmotes(); - AccessGuard accessGlobalEmotes() const; - boost::optional getGlobalEmote(const EmoteName &name); - boost::optional getEmote(const EmoteId &id); - - void loadGlobalEmotes(); + std::shared_ptr emotes() const; + boost::optional emote(const EmoteName &name) const; + void loadEmotes(); + static void loadChannel(const QString &channelName, + std::function callback); private: - std::pair parseGlobalEmotes( - const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes); - - UniqueAccess globalEmotes_; - // UniqueAccess channelEmoteCache_; + Atomic> global_; }; } // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.cpp b/src/providers/bttv/LoadBttvChannelEmote.cpp index ccc52a130..f5116cb02 100644 --- a/src/providers/bttv/LoadBttvChannelEmote.cpp +++ b/src/providers/bttv/LoadBttvChannelEmote.cpp @@ -11,78 +11,4 @@ namespace chatterino { -static Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale); -static std::pair bttvParseChannelEmotes( - const QJsonObject &jsonRoot); - -void loadBttvChannelEmotes(const QString &channelName, - std::function callback) -{ - auto request = - NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelName); - - request.setCaller(QThread::currentThread()); - request.setTimeout(3000); - request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { - auto pair = bttvParseChannelEmotes(result.parseJson()); - - if (pair.first == Success) callback(std::move(pair.second)); - - return pair.first; - }); - - request.execute(); -} - -static std::pair bttvParseChannelEmotes( - const QJsonObject &jsonRoot) -{ - static UniqueAccess>> - cache_; - - auto cache = cache_.access(); - auto emotes = EmoteMap(); - auto jsonEmotes = jsonRoot.value("emotes").toArray(); - auto urlTemplate = - QString("https:" + jsonRoot.value("urlTemplate").toString()); - - for (auto jsonEmote_ : jsonEmotes) { - auto jsonEmote = jsonEmote_.toObject(); - - auto id = EmoteId{jsonEmote.value("id").toString()}; - auto name = EmoteName{jsonEmote.value("code").toString()}; - // emoteObject.value("imageType").toString(); - - auto emote = Emote( - {name, - ImageSet{ - Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), - Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), - Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, - Tooltip{name.string + "
Channel Bttv Emote"}, - Url{"https://manage.betterttv.net/emotes/" + id.string}}); - - auto shared = (*cache)[id].lock(); - if (shared && *shared == emote) { - // reuse old shared_ptr if nothing changed - emotes[name] = shared; - } else { - (*cache)[id] = emotes[name] = - std::make_shared(std::move(emote)); - } - } - - return {Success, std::move(emotes)}; -} - -static Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) -{ - urlTemplate.detach(); - - return {urlTemplate.replace("{{id}}", id.string) - .replace("{{image}}", emoteScale)}; -} - } // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.hpp b/src/providers/bttv/LoadBttvChannelEmote.hpp index ffbb50469..d0fa5cc32 100644 --- a/src/providers/bttv/LoadBttvChannelEmote.hpp +++ b/src/providers/bttv/LoadBttvChannelEmote.hpp @@ -6,11 +6,4 @@ class QString; namespace chatterino { -class EmoteMap; -constexpr const char *bttvChannelEmoteApiUrl = - "https://api.betterttv.net/2/channels/"; - -void loadBttvChannelEmotes(const QString &channelName, - std::function callback); - } // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.cpp b/src/providers/chatterino/ChatterinoBadges.cpp index 98b1841d4..37dc8f6f0 100644 --- a/src/providers/chatterino/ChatterinoBadges.cpp +++ b/src/providers/chatterino/ChatterinoBadges.cpp @@ -14,39 +14,41 @@ ChatterinoBadges::ChatterinoBadges() boost::optional ChatterinoBadges::getBadge(const UserName &username) { - return this->badges.access()->get(username); + return boost::none; + // return this->badges.access()->get(username); } void ChatterinoBadges::loadChatterinoBadges() { - static QString url("https://fourtf.com/chatterino/badges.json"); + // static QString url("https://fourtf.com/chatterino/badges.json"); - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); + // NetworkRequest req(url); + // req.setCaller(QThread::currentThread()); - req.onSuccess([this](auto result) { - auto jsonRoot = result.parseJson(); - auto badges = this->badges.access(); - auto replacement = badges->makeReplacment(); + // req.onSuccess([this](auto result) { + // auto jsonRoot = result.parseJson(); + // auto badges = this->badges.access(); + // auto replacement = badges->makeReplacment(); - for (auto jsonBadge_ : jsonRoot.value("badges").toArray()) { - auto jsonBadge = jsonBadge_.toObject(); + // for (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{}}; + // auto emote = Emote{ + // EmoteName{}, + // ImageSet{Url{jsonBadge.value("image").toString()}}, + // Tooltip{jsonBadge.value("tooltip").toString()}, Url{}}; - for (auto jsonUser : jsonBadge.value("users").toArray()) { - replacement.add(UserName{jsonUser.toString()}, - std::move(emote)); - } - } + // for (auto jsonUser : jsonBadge.value("users").toArray()) { + // replacement.add(UserName{jsonUser.toString()}, + // std::move(emote)); + // } + // } - replacement.apply(); - return Success; - }); + // replacement.apply(); + // return Success; + //}); - req.execute(); + // req.execute(); } } // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.hpp b/src/providers/chatterino/ChatterinoBadges.hpp index 3162c75b1..89870fd16 100644 --- a/src/providers/chatterino/ChatterinoBadges.hpp +++ b/src/providers/chatterino/ChatterinoBadges.hpp @@ -1,14 +1,14 @@ #pragma once #include -#include -#include "common/Common.hpp" -#include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" -#include "messages/EmoteCache.hpp" + +#include "common/Aliases.hpp" namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + class ChatterinoBadges { public: @@ -19,7 +19,7 @@ public: private: void loadChatterinoBadges(); - UniqueAccess> badges; + // UniqueAccess> badges; }; } // namespace chatterino diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index ec7bac2b8..809b7c3d5 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "debug/Log.hpp" +#include "messages/Emote.hpp" #include "singletons/Settings.hpp" #include @@ -12,82 +13,81 @@ #include namespace chatterino { - namespace { + void parseEmoji(const std::shared_ptr &emojiData, + const rapidjson::Value &unparsedEmoji, + QString shortCode = QString()) + { + static uint unicodeBytes[4]; -void parseEmoji(const std::shared_ptr &emojiData, - const rapidjson::Value &unparsedEmoji, - QString shortCode = QString()) -{ - static uint unicodeBytes[4]; + struct { + bool apple; + bool google; + bool twitter; + bool emojione; + bool facebook; + bool messenger; + } capabilities; - struct { - bool apple; - bool google; - bool twitter; - bool emojione; - bool facebook; - bool messenger; - } capabilities; - - if (!shortCode.isEmpty()) { - emojiData->shortCodes.push_back(shortCode); - } else { - const auto &shortCodes = unparsedEmoji["short_names"]; - for (const auto &shortCode : shortCodes.GetArray()) { - emojiData->shortCodes.emplace_back(shortCode.GetString()); + if (!shortCode.isEmpty()) { + emojiData->shortCodes.push_back(shortCode); + } else { + const auto &shortCodes = unparsedEmoji["short_names"]; + for (const auto &shortCode : shortCodes.GetArray()) { + emojiData->shortCodes.emplace_back(shortCode.GetString()); + } } - } - rj::getSafe(unparsedEmoji, "non_qualified", emojiData->nonQualifiedCode); - rj::getSafe(unparsedEmoji, "unified", emojiData->unifiedCode); + rj::getSafe(unparsedEmoji, "non_qualified", + emojiData->nonQualifiedCode); + rj::getSafe(unparsedEmoji, "unified", emojiData->unifiedCode); - rj::getSafe(unparsedEmoji, "has_img_apple", capabilities.apple); - rj::getSafe(unparsedEmoji, "has_img_google", capabilities.google); - rj::getSafe(unparsedEmoji, "has_img_twitter", capabilities.twitter); - rj::getSafe(unparsedEmoji, "has_img_emojione", capabilities.emojione); - rj::getSafe(unparsedEmoji, "has_img_facebook", capabilities.facebook); - rj::getSafe(unparsedEmoji, "has_img_messenger", capabilities.messenger); + rj::getSafe(unparsedEmoji, "has_img_apple", capabilities.apple); + rj::getSafe(unparsedEmoji, "has_img_google", capabilities.google); + rj::getSafe(unparsedEmoji, "has_img_twitter", capabilities.twitter); + rj::getSafe(unparsedEmoji, "has_img_emojione", capabilities.emojione); + rj::getSafe(unparsedEmoji, "has_img_facebook", capabilities.facebook); + rj::getSafe(unparsedEmoji, "has_img_messenger", capabilities.messenger); - if (capabilities.apple) { - emojiData->capabilities.insert("Apple"); - } - if (capabilities.google) { - emojiData->capabilities.insert("Google"); - } - if (capabilities.twitter) { - emojiData->capabilities.insert("Twitter"); - } - if (capabilities.emojione) { - emojiData->capabilities.insert("EmojiOne 3"); - } - if (capabilities.facebook) { - emojiData->capabilities.insert("Facebook"); - } - if (capabilities.messenger) { - emojiData->capabilities.insert("Messenger"); - } + if (capabilities.apple) { + emojiData->capabilities.insert("Apple"); + } + if (capabilities.google) { + emojiData->capabilities.insert("Google"); + } + if (capabilities.twitter) { + emojiData->capabilities.insert("Twitter"); + } + if (capabilities.emojione) { + emojiData->capabilities.insert("EmojiOne 3"); + } + if (capabilities.facebook) { + emojiData->capabilities.insert("Facebook"); + } + if (capabilities.messenger) { + emojiData->capabilities.insert("Messenger"); + } - QStringList unicodeCharacters; - if (!emojiData->nonQualifiedCode.isEmpty()) { - unicodeCharacters = emojiData->nonQualifiedCode.toLower().split('-'); - } else { - unicodeCharacters = emojiData->unifiedCode.toLower().split('-'); + QStringList unicodeCharacters; + if (!emojiData->nonQualifiedCode.isEmpty()) { + unicodeCharacters = + emojiData->nonQualifiedCode.toLower().split('-'); + } else { + unicodeCharacters = emojiData->unifiedCode.toLower().split('-'); + } + if (unicodeCharacters.length() < 1) { + return; + } + + int numUnicodeBytes = 0; + + for (const QString &unicodeCharacter : unicodeCharacters) { + unicodeBytes[numUnicodeBytes++] = + QString(unicodeCharacter).toUInt(nullptr, 16); + } + + emojiData->value = QString::fromUcs4(unicodeBytes, numUnicodeBytes); } - if (unicodeCharacters.length() < 1) { - return; - } - - int numUnicodeBytes = 0; - - for (const QString &unicodeCharacter : unicodeCharacters) { - unicodeBytes[numUnicodeBytes++] = - QString(unicodeCharacter).toUInt(nullptr, 16); - } - - emojiData->value = QString::fromUcs4(unicodeBytes, numUnicodeBytes); -} - } // namespace void Emojis::load() @@ -103,12 +103,10 @@ void Emojis::load() void Emojis::loadEmojis() { - std::map toneNames; - toneNames["1F3FB"] = "tone1"; - toneNames["1F3FC"] = "tone2"; - toneNames["1F3FD"] = "tone3"; - toneNames["1F3FE"] = "tone4"; - toneNames["1F3FF"] = "tone5"; + auto toneNames = std::map{ + {"1F3FB", "tone1"}, {"1F3FC", "tone2"}, {"1F3FD", "tone3"}, + {"1F3FE", "tone4"}, {"1F3FF", "tone5"}, + }; QFile file(":/emoji.json"); file.open(QFile::ReadOnly); @@ -118,7 +116,7 @@ void Emojis::loadEmojis() rapidjson::ParseResult result = root.Parse(data.toUtf8(), data.length()); if (result.Code() != rapidjson::kParseErrorNone) { - Log("JSON parse error: {} ({})", + log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), result.Offset()); return; } @@ -146,7 +144,7 @@ void Emojis::loadEmojis() auto toneNameIt = toneNames.find(tone); if (toneNameIt == toneNames.end()) { - Log("Tone with key {} does not exist in tone names map", + log("Tone with key {} does not exist in tone names map", tone); continue; } @@ -218,8 +216,8 @@ void Emojis::loadEmojiSet() { auto app = getApp(); - app->settings->emojiSet.connect([=](const auto &emojiSet, auto) { - Log("Using emoji set {}", emojiSet); + getSettings()->emojiSet.connect([=](const auto &emojiSet, auto) { + log("Using emoji set {}", emojiSet); this->emojis.each([=](const auto &name, std::shared_ptr &emoji) { QString emojiSetToUse = emojiSet; @@ -233,27 +231,12 @@ void Emojis::loadEmojiSet() // {"Google", "https://cdn.jsdelivr.net/npm/emoji-datasource-google@4.0.4/img/google/64/"}, // {"Messenger", "https://cdn.jsdelivr.net/npm/emoji-datasource-messenger@4.0.4/img/messenger/64/"}, - // {"EmojiOne 2", "https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.6/assets/png/"}, - // {"EmojiOne 3", "https://braize.pajlada.com/emoji/img/emojione/64/"}, - // {"Twitter", "https://braize.pajlada.com/emoji/img/twitter/64/"}, - // {"Facebook", "https://braize.pajlada.com/emoji/img/facebook/64/"}, - // {"Apple", "https://braize.pajlada.com/emoji/img/apple/64/"}, - // {"Google", "https://braize.pajlada.com/emoji/img/google/64/"}, - // {"Messenger", "https://braize.pajlada.com/emoji/img/messenger/64/"}, - {"EmojiOne 3", "https://pajbot.com/static/emoji/img/emojione/64/"}, {"Twitter", "https://pajbot.com/static/emoji/img/twitter/64/"}, {"Facebook", "https://pajbot.com/static/emoji/img/facebook/64/"}, {"Apple", "https://pajbot.com/static/emoji/img/apple/64/"}, {"Google", "https://pajbot.com/static/emoji/img/google/64/"}, {"Messenger", "https://pajbot.com/static/emoji/img/messenger/64/"}, - -// {"EmojiOne 3", "https://cdn.fourtf.com/emoji/emojione/64/"}, -// {"Twitter", "https://cdn.fourtf.com/emoji/twitter/64/"}, -// {"Facebook", "https://cdn.fourtf.com/emoji/facebook/64/"}, -// {"Apple", "https://cdn.fourtf.com/emoji/apple/64/"}, -// {"Google", "https://cdn.fourtf.com/emoji/google/64/"}, -// {"Messenger", "https://cdn.fourtf.com/emoji/messenger/64/"}, }; // clang-format on diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index 6a2f4e74e..62443b9aa 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -1,8 +1,5 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" -#include "messages/Emote.hpp" #include "util/ConcurrentMap.hpp" #include @@ -14,6 +11,9 @@ namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + struct EmojiData { // actual byte-representation of the emoji (i.e. \154075\156150 which is // :male:) diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index b5fe65aa2..eb6912ff7 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -3,172 +3,164 @@ #include #include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" #include "debug/Log.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" namespace chatterino { namespace { -Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) -{ - auto emote = urls.value(emoteScale); - if (emote.isUndefined()) { - return {""}; + Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) + { + auto emote = urls.value(emoteScale); + if (emote.isUndefined()) { + return {""}; + } + + assert(emote.isString()); + + return {"https:" + emote.toString()}; } + void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, + const QString &tooltip, Emote &emoteData) + { + auto url1x = getEmoteLink(urls, "1"); + auto url2x = getEmoteLink(urls, "2"); + auto url3x = getEmoteLink(urls, "4"); - assert(emote.isString()); + //, code, tooltip + emoteData.name = name; + emoteData.images = + ImageSet{Image::fromUrl(url1x, 1), Image::fromUrl(url2x, 0.5), + Image::fromUrl(url3x, 0.25)}; + emoteData.tooltip = {tooltip}; + } + EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) + { + static std::unordered_map> cache; + static std::mutex mutex; - return {"https:" + emote.toString()}; -} + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); + } + std::pair parseGlobalEmotes( + const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes) + { + auto jsonSets = jsonRoot.value("sets").toObject(); + auto emotes = EmoteMap(); -void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, - const QString &tooltip, Emote &emoteData) -{ - auto url1x = getEmoteLink(urls, "1"); - auto url2x = getEmoteLink(urls, "2"); - auto url3x = getEmoteLink(urls, "4"); + for (auto jsonSet : jsonSets) { + auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); - //, code, tooltip - emoteData.name = name; - emoteData.images = - ImageSet{Image::fromUrl(url1x, 1), Image::fromUrl(url2x, 0.5), - Image::fromUrl(url3x, 0.25)}; - emoteData.tooltip = {tooltip}; -} + for (auto jsonEmoteValue : jsonEmotes) { + auto jsonEmote = jsonEmoteValue.toObject(); + + auto name = EmoteName{jsonEmote.value("name").toString()}; + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto urls = jsonEmote.value("urls").toObject(); + + auto emote = Emote(); + fillInEmoteData(urls, name, + name.string + "
Global FFZ Emote", emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + emotes[name] = + cachedOrMakeEmotePtr(std::move(emote), currentEmotes); + } + } + + return {Success, std::move(emotes)}; + } + std::pair parseChannelEmotes(const QJsonObject &jsonRoot) + { + auto jsonSets = jsonRoot.value("sets").toObject(); + auto emotes = EmoteMap(); + + for (auto jsonSet : jsonSets) { + auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); + + for (auto _jsonEmote : jsonEmotes) { + auto jsonEmote = _jsonEmote.toObject(); + + // margins + auto id = + EmoteId{QString::number(jsonEmote.value("id").toInt())}; + auto name = EmoteName{jsonEmote.value("name").toString()}; + auto urls = jsonEmote.value("urls").toObject(); + + Emote emote; + fillInEmoteData(urls, name, + name.string + "
Channel FFZ Emote", emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + emotes[name] = cachedOrMake(std::move(emote), id); + } + } + + return {Success, std::move(emotes)}; + } } // namespace -AccessGuard> FfzEmotes::accessGlobalEmotes() const +FfzEmotes::FfzEmotes() + : global_(std::make_shared()) { - return this->globalEmotes_.accessConst(); } -boost::optional FfzEmotes::getEmote(const EmoteId &id) +std::shared_ptr FfzEmotes::emotes() const { - auto cache = this->channelEmoteCache_.access(); - auto it = cache->find(id); - - if (it != cache->end()) { - auto shared = it->second.lock(); - if (shared) { - return shared; - } - } + return this->global_.get(); +} +boost::optional FfzEmotes::emote(const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + if (it != emotes->end()) return it->second; return boost::none; } -boost::optional FfzEmotes::getGlobalEmote(const EmoteName &name) -{ - return this->globalEmotes_.access()->get(name); -} - -void FfzEmotes::loadGlobalEmotes() +void FfzEmotes::loadEmotes() { QString url("https://api.frankerfacez.com/v1/set/global"); NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(30000); + request.onSuccess([this](auto result) -> Outcome { - return this->parseGlobalEmotes(result.parseJson()); + auto emotes = this->emotes(); + auto pair = parseGlobalEmotes(result.parseJson(), *emotes); + if (pair.first) + this->global_.set( + std::make_shared(std::move(pair.second))); + return pair.first; }); request.execute(); } -Outcome FfzEmotes::parseGlobalEmotes(const QJsonObject &jsonRoot) +void FfzEmotes::loadChannel(const QString &channelName, + std::function callback) { - auto jsonSets = jsonRoot.value("sets").toObject(); - auto emotes = this->globalEmotes_.access(); - auto replacement = emotes->makeReplacment(); + log("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", channelName); - for (auto jsonSet : jsonSets) { - auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); + NetworkRequest request("https://api.frankerfacez.com/v1/room/" + + channelName); + request.setCaller(QThread::currentThread()); + request.setTimeout(3000); - for (auto jsonEmoteValue : jsonEmotes) { - auto jsonEmote = jsonEmoteValue.toObject(); + request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { + auto pair = parseChannelEmotes(result.parseJson()); + if (pair.first) callback(std::move(pair.second)); + return pair.first; + }); - auto name = EmoteName{jsonEmote.value("name").toString()}; - auto id = EmoteId{jsonEmote.value("id").toString()}; - auto urls = jsonEmote.value("urls").toObject(); - - auto emote = Emote(); - fillInEmoteData(urls, name, name.string + "
Global FFZ Emote", - emote); - emote.homePage = - Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") - .arg(id.string) - .arg(name.string)}; - - replacement.add(name, emote); - } - } - - return Success; -} - -void FfzEmotes::loadChannelEmotes(const QString &channelName, - std::function callback) -{ - // printf("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", - // qPrintable(channelName)); - - // QString url("https://api.frankerfacez.com/v1/room/" + channelName); - - // NetworkRequest request(url); - // request.setCaller(QThread::currentThread()); - // request.setTimeout(3000); - // request.onSuccess([this, channelName, _map](auto result) -> Outcome { - // return this->parseChannelEmotes(result.parseJson()); - //}); - - // request.execute(); -} - -Outcome parseChannelEmotes(const QJsonObject &jsonRoot) -{ - // auto rootNode = result.parseJson(); - // auto map = _map.lock(); - - // if (_map.expired()) { - // return false; - //} - - // map->clear(); - - // auto setsNode = rootNode.value("sets").toObject(); - - // std::vector codes; - // for (const QJsonValue &setNode : setsNode) { - // auto emotesNode = setNode.toObject().value("emoticons").toArray(); - - // for (const QJsonValue &emoteNode : emotesNode) { - // QJsonObject emoteObject = emoteNode.toObject(); - - // // margins - // int id = emoteObject.value("id").toInt(); - // QString code = emoteObject.value("name").toString(); - - // QJsonObject urls = emoteObject.value("urls").toObject(); - - // auto emote = this->channelEmoteCache_.getOrAdd(id, [id, &code, - // &urls] { - // EmoteData emoteData; - // fillInEmoteData(urls, code, code + "
Channel FFZ Emote", - // emoteData); emoteData.pageLink = - // QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); - - // return emoteData; - // }); - - // this->channelEmotes.insert(code, emote); - // map->insert(code, emote); - // codes.push_back(code); - // } - - // this->channelEmoteCodes[channelName] = codes; - //} - - return Success; + request.execute(); } } // namespace chatterino diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index 2dd747e02..8214cbf80 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -1,39 +1,32 @@ #pragma once #include - -#include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" -#include "messages/EmoteCache.hpp" +#include "boost/optional.hpp" +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" namespace chatterino { -class FfzEmotes final : std::enable_shared_from_this +struct Emote; +using EmotePtr = std::shared_ptr; +class EmoteMap; + +class FfzEmotes final { static constexpr const char *globalEmoteApiUrl = "https://api.frankerfacez.com/v1/set/global"; - static constexpr const char *channelEmoteApiUrl = - "https://api.betterttv.net/2/channels/"; public: - // FfzEmotes(); + FfzEmotes(); - static std::shared_ptr create(); + std::shared_ptr emotes() const; + boost::optional emote(const EmoteName &name) const; + void loadEmotes(); + static void loadChannel(const QString &channelName, + std::function callback); - AccessGuard> accessGlobalEmotes() const; - boost::optional getGlobalEmote(const EmoteName &name); - boost::optional getEmote(const EmoteId &id); - - void loadGlobalEmotes(); - void loadChannelEmotes(const QString &channelName, - std::function callback); - -protected: - Outcome parseGlobalEmotes(const QJsonObject &jsonRoot); - Outcome parseChannelEmotes(const QJsonObject &jsonRoot); - - UniqueAccess> globalEmotes_; - UniqueAccess channelEmoteCache_; +private: + Atomic> global_; }; } // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 23bf47d26..25c810a4d 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -1,6 +1,8 @@ #include "AbstractIrcServer.hpp" +#include "common/Channel.hpp" #include "common/Common.hpp" +#include "debug/Log.hpp" #include "messages/LimitedQueueSnapshot.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" @@ -131,7 +133,7 @@ std::shared_ptr AbstractIrcServer::getOrAddChannel( chan->destroyed.connect([this, clojuresInCppAreShit] { // fourtf: issues when the server itself is destroyed - Log("[AbstractIrcServer::addChannel] {} was destroyed", + log("[AbstractIrcServer::addChannel] {} was destroyed", clojuresInCppAreShit); this->channels.remove(clojuresInCppAreShit); diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 580cdd0fe..109f869ef 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -1,6 +1,5 @@ #pragma once -#include "common/Channel.hpp" #include "providers/irc/IrcConnection2.hpp" #include @@ -11,6 +10,9 @@ namespace chatterino { +class Channel; +using ChannelPtr = std::shared_ptr; + class AbstractIrcServer { public: diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 5d628e311..9aabbc91d 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -10,6 +10,7 @@ #include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/IrcHelpers.hpp" @@ -145,7 +146,7 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) auto chan = app->twitch.server->getChannelOrEmpty(chanName); if (chan->isEmpty()) { - Log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not " + log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not " "found", chanName); return; @@ -209,7 +210,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) { auto app = getApp(); - Log("Received whisper!"); + log("Received whisper!"); MessageParseArgs args; args.isReceivedWhisper = true; @@ -230,7 +231,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) c->addMessage(_message); - if (app->settings->inlineWhispers) { + if (getSettings()->inlineWhispers) { app->twitch.server->forEachChannel([_message](ChannelPtr channel) { channel->addMessage(_message); // }); @@ -326,7 +327,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) auto channel = app->twitch.server->getChannelOrEmpty(channelName); if (channel->isEmpty()) { - Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel " + log("[IrcManager:handleNoticeMessage] Channel {} not found in channel " "manager ", channelName); return; @@ -366,7 +367,7 @@ void IrcMessageHandler::handleWriteConnectionNoticeMessage( return; } - Log("Showing notice message from write connection with message id '{}'", + log("Showing notice message from write connection with message id '{}'", msgID); } diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp index 98fa2ba42..6aca624b1 100644 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -1,5 +1,6 @@ #include "providers/twitch/PartialTwitchUser.hpp" +#include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -42,23 +43,23 @@ void PartialTwitchUser::getId(std::function successCallback, request.onSuccess([successCallback](auto result) -> Outcome { auto root = result.parseJson(); if (!root.value("users").isArray()) { - Log("API Error while getting user id, users is not an array"); + log("API Error while getting user id, users is not an array"); return Failure; } auto users = root.value("users").toArray(); if (users.size() != 1) { - Log("API Error while getting user id, users array size is not 1"); + log("API Error while getting user id, users array size is not 1"); return Failure; } if (!users[0].isObject()) { - Log("API Error while getting user id, first user is not an object"); + log("API Error while getting user id, first user is not an object"); return Failure; } auto firstUser = users[0].toObject(); auto id = firstUser.value("_id"); if (!id.isString()) { - Log("API Error: while getting user id, first user object `_id` key " + log("API Error: while getting user id, first user object `_id` key " "is not a " "string"); return Failure; diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 7cdd343ce..77b6ba1d4 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -24,157 +24,159 @@ static std::map sentMessages; namespace detail { -PubSubClient::PubSubClient(WebsocketClient &websocketClient, - WebsocketHandle handle) - : websocketClient_(websocketClient) - , handle_(handle) -{ -} - -void PubSubClient::start() -{ - assert(!this->started_); - - this->started_ = true; - - this->ping(); -} - -void PubSubClient::stop() -{ - assert(this->started_); - - this->started_ = false; -} - -bool PubSubClient::listen(rapidjson::Document &message) -{ - int numRequestedListens = message["data"]["topics"].Size(); - - if (this->numListens_ + numRequestedListens > MAX_PUBSUB_LISTENS) { - // This PubSubClient is already at its peak listens - return false; + PubSubClient::PubSubClient(WebsocketClient &websocketClient, + WebsocketHandle handle) + : websocketClient_(websocketClient) + , handle_(handle) + { } - this->numListens_ += numRequestedListens; + void PubSubClient::start() + { + assert(!this->started_); - for (const auto &topic : message["data"]["topics"].GetArray()) { - this->listeners_.emplace_back( - Listener{topic.GetString(), false, false, false}); + this->started_ = true; + + this->ping(); } - auto uuid = CreateUUID(); + void PubSubClient::stop() + { + assert(this->started_); - rj::set(message, "nonce", uuid); + this->started_ = false; + } - std::string payload = rj::stringify(message); - sentMessages[uuid] = payload; + bool PubSubClient::listen(rapidjson::Document &message) + { + int numRequestedListens = message["data"]["topics"].Size(); - this->send(payload.c_str()); - - return true; -} - -void PubSubClient::unlistenPrefix(const std::string &prefix) -{ - std::vector topics; - - for (auto it = this->listeners_.begin(); it != this->listeners_.end();) { - const auto &listener = *it; - if (listener.topic.find(prefix) == 0) { - topics.push_back(listener.topic); - it = this->listeners_.erase(it); - } else { - ++it; + if (this->numListens_ + numRequestedListens > MAX_PUBSUB_LISTENS) { + // This PubSubClient is already at its peak listens + return false; } - } - if (topics.empty()) { - return; - } + this->numListens_ += numRequestedListens; - auto message = createUnlistenMessage(topics); - - auto uuid = CreateUUID(); - - rj::set(message, "nonce", CreateUUID()); - - std::string payload = rj::stringify(message); - sentMessages[uuid] = payload; - - this->send(payload.c_str()); -} - -void PubSubClient::handlePong() -{ - assert(this->awaitingPong_); - - Log("Got pong!"); - - this->awaitingPong_ = false; -} - -bool PubSubClient::isListeningToTopic(const std::string &payload) -{ - for (const auto &listener : this->listeners_) { - if (listener.topic == payload) { - return true; + for (const auto &topic : message["data"]["topics"].GetArray()) { + this->listeners_.emplace_back( + Listener{topic.GetString(), false, false, false}); } + + auto uuid = CreateUUID(); + + rj::set(message, "nonce", uuid); + + std::string payload = rj::stringify(message); + sentMessages[uuid] = payload; + + this->send(payload.c_str()); + + return true; } - return false; -} + void PubSubClient::unlistenPrefix(const std::string &prefix) + { + std::vector topics; -void PubSubClient::ping() -{ - assert(this->started_); + for (auto it = this->listeners_.begin(); + it != this->listeners_.end();) { + const auto &listener = *it; + if (listener.topic.find(prefix) == 0) { + topics.push_back(listener.topic); + it = this->listeners_.erase(it); + } else { + ++it; + } + } - if (!this->send(pingPayload)) { - return; + if (topics.empty()) { + return; + } + + auto message = createUnlistenMessage(topics); + + auto uuid = CreateUUID(); + + rj::set(message, "nonce", CreateUUID()); + + std::string payload = rj::stringify(message); + sentMessages[uuid] = payload; + + this->send(payload.c_str()); } - this->awaitingPong_ = true; + void PubSubClient::handlePong() + { + assert(this->awaitingPong_); - auto self = this->shared_from_this(); + log("Got pong!"); - runAfter(this->websocketClient_.get_io_service(), std::chrono::seconds(15), - [self](auto timer) { - if (!self->started_) { - return; - } + this->awaitingPong_ = false; + } - if (self->awaitingPong_) { - Log("No pong respnose, disconnect!"); - // TODO(pajlada): Label this connection as "disconnect me" - } - }); - - runAfter(this->websocketClient_.get_io_service(), std::chrono::minutes(5), - [self](auto timer) { - if (!self->started_) { - return; - } - - self->ping(); // - }); -} - -bool PubSubClient::send(const char *payload) -{ - WebsocketErrorCode ec; - this->websocketClient_.send(this->handle_, payload, - websocketpp::frame::opcode::text, ec); - - if (ec) { - Log("Error sending message {}: {}", payload, ec.message()); - // TODO(pajlada): Check which error code happened and maybe gracefully - // handle it + bool PubSubClient::isListeningToTopic(const std::string &payload) + { + for (const auto &listener : this->listeners_) { + if (listener.topic == payload) { + return true; + } + } return false; } - return true; -} + void PubSubClient::ping() + { + assert(this->started_); + + if (!this->send(pingPayload)) { + return; + } + + this->awaitingPong_ = true; + + auto self = this->shared_from_this(); + + runAfter(this->websocketClient_.get_io_service(), + std::chrono::seconds(15), [self](auto timer) { + if (!self->started_) { + return; + } + + if (self->awaitingPong_) { + log("No pong respnose, disconnect!"); + // TODO(pajlada): Label this connection as "disconnect + // me" + } + }); + + runAfter(this->websocketClient_.get_io_service(), + std::chrono::minutes(5), [self](auto timer) { + if (!self->started_) { + return; + } + + self->ping(); // + }); + } + + bool PubSubClient::send(const char *payload) + { + WebsocketErrorCode ec; + this->websocketClient_.send(this->handle_, payload, + websocketpp::frame::opcode::text, ec); + + if (ec) { + log("Error sending message {}: {}", payload, ec.message()); + // TODO(pajlada): Check which error code happened and maybe + // gracefully handle it + + return false; + } + + return true; + } } // namespace detail @@ -207,26 +209,26 @@ PubSub::PubSub() action.state = ModeChangedAction::State::On; if (!data.HasMember("args")) { - Log("Missing required args member"); + log("Missing required args member"); return; } const auto &args = data["args"]; if (!args.IsArray()) { - Log("args member must be an array"); + log("args member must be an array"); return; } if (args.Size() == 0) { - Log("Missing duration argument in slowmode on"); + log("Missing duration argument in slowmode on"); return; } const auto &durationArg = args[0]; if (!durationArg.IsString()) { - Log("Duration arg must be a string"); + log("Duration arg must be a string"); return; } @@ -314,7 +316,7 @@ PubSub::PubSub() return; } } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } action.modded = false; @@ -339,7 +341,7 @@ PubSub::PubSub() return; } } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } action.modded = true; @@ -380,7 +382,7 @@ PubSub::PubSub() this->signals_.moderation.userBanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; @@ -410,7 +412,7 @@ PubSub::PubSub() this->signals_.moderation.userBanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; @@ -436,7 +438,7 @@ PubSub::PubSub() this->signals_.moderation.userUnbanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; @@ -462,7 +464,7 @@ PubSub::PubSub() this->signals_.moderation.userUnbanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; @@ -493,7 +495,7 @@ void PubSub::addClient() auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, ec); if (ec) { - Log("Unable to establish connection: {}", ec.message()); + log("Unable to establish connection: {}", ec.message()); return; } @@ -512,7 +514,7 @@ void PubSub::listenToWhispers(std::shared_ptr account) std::string userID = account->getUserId().toStdString(); - Log("Connection open!"); + log("Connection open!"); websocketpp::lib::error_code ec; std::vector topics({"whispers." + userID}); @@ -520,7 +522,7 @@ void PubSub::listenToWhispers(std::shared_ptr account) this->listen(createListenMessage(topics, account)); if (ec) { - Log("Unable to send message to websocket server: {}", ec.message()); + log("Unable to send message to websocket server: {}", ec.message()); return; } } @@ -544,11 +546,11 @@ void PubSub::listenToChannelModerationActions( std::string topic(fS("chat_moderator_actions.{}.{}", userID, channelID)); if (this->isListeningToTopic(topic)) { - Log("We are already listening to topic {}", topic); + log("We are already listening to topic {}", topic); return; } - Log("Listen to topic {}", topic); + log("Listen to topic {}", topic); this->listenToTopic(topic, account); } @@ -564,18 +566,18 @@ void PubSub::listenToTopic(const std::string &topic, void PubSub::listen(rapidjson::Document &&msg) { if (this->tryListen(msg)) { - Log("Successfully listened!"); + log("Successfully listened!"); return; } - Log("Added to the back of the queue"); + log("Added to the back of the queue"); this->requests.emplace_back( std::make_unique(std::move(msg))); } bool PubSub::tryListen(rapidjson::Document &msg) { - Log("tryListen with {} clients", this->clients.size()); + log("tryListen with {} clients", this->clients.size()); for (const auto &p : this->clients) { const auto &client = p.second; if (client->listen(msg)) { @@ -608,13 +610,13 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, rapidjson::ParseResult res = msg.Parse(payload.c_str()); if (!res) { - Log("Error parsing message '{}' from PubSub: {}", payload, + log("Error parsing message '{}' from PubSub: {}", payload, rapidjson::GetParseError_En(res.Code())); return; } if (!msg.IsObject()) { - Log("Error parsing message '{}' from PubSub. Root object is not an " + log("Error parsing message '{}' from PubSub. Root object is not an " "object", payload); return; @@ -623,7 +625,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, std::string type; if (!rj::getSafe(msg, "type", type)) { - Log("Missing required string member `type` in message root"); + log("Missing required string member `type` in message root"); return; } @@ -631,14 +633,14 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, this->handleListenResponse(msg); } else if (type == "MESSAGE") { if (!msg.HasMember("data")) { - Log("Missing required object member `data` in message root"); + log("Missing required object member `data` in message root"); return; } const auto &data = msg["data"]; if (!data.IsObject()) { - Log("Member `data` must be an object"); + log("Member `data` must be an object"); return; } @@ -654,7 +656,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, client.second->handlePong(); } else { - Log("Unknown message type: {}", type); + log("Unknown message type: {}", type); } } @@ -699,7 +701,7 @@ PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl) boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); } catch (const std::exception &e) { - Log("Exception caught in OnTLSInit: {}", e.what()); + log("Exception caught in OnTLSInit: {}", e.what()); } return ctx; @@ -714,12 +716,12 @@ void PubSub::handleListenResponse(const rapidjson::Document &msg) rj::getSafe(msg, "nonce", nonce); if (error.empty()) { - Log("Successfully listened to nonce {}", nonce); + log("Successfully listened to nonce {}", nonce); // Nothing went wrong return; } - Log("PubSub error: {} on nonce {}", error, nonce); + log("PubSub error: {} on nonce {}", error, nonce); return; } } @@ -729,14 +731,14 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) QString topic; if (!rj::getSafe(outerData, "topic", topic)) { - Log("Missing required string member `topic` in outerData"); + log("Missing required string member `topic` in outerData"); return; } std::string payload; if (!rj::getSafe(outerData, "message", payload)) { - Log("Expected string message in outerData"); + log("Expected string message in outerData"); return; } @@ -745,7 +747,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) rapidjson::ParseResult res = msg.Parse(payload.c_str()); if (!res) { - Log("Error parsing message '{}' from PubSub: {}", payload, + log("Error parsing message '{}' from PubSub: {}", payload, rapidjson::GetParseError_En(res.Code())); return; } @@ -754,7 +756,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) std::string whisperType; if (!rj::getSafe(msg, "type", whisperType)) { - Log("Bad whisper data"); + log("Bad whisper data"); return; } @@ -765,7 +767,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } else if (whisperType == "thread") { // Handle thread? } else { - Log("Invalid whisper type: {}", whisperType); + log("Invalid whisper type: {}", whisperType); assert(false); return; } @@ -777,30 +779,30 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) std::string moderationAction; if (!rj::getSafe(data, "moderation_action", moderationAction)) { - Log("Missing moderation action in data: {}", rj::stringify(data)); + log("Missing moderation action in data: {}", rj::stringify(data)); return; } auto handlerIt = this->moderationActionHandlers.find(moderationAction); if (handlerIt == this->moderationActionHandlers.end()) { - Log("No handler found for moderation action {}", moderationAction); + log("No handler found for moderation action {}", moderationAction); return; } // Invoke handler function handlerIt->second(data, topicParts[2]); } else { - Log("Unknown topic: {}", topic); + log("Unknown topic: {}", topic); return; } } void PubSub::runThread() { - Log("Start pubsub manager thread"); + log("Start pubsub manager thread"); this->websocketClient.run(); - Log("Done with pubsub manager thread"); + log("Done with pubsub manager thread"); } } // namespace chatterino diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index 211502176..f3cc10f15 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -32,41 +32,42 @@ using WebsocketErrorCode = websocketpp::lib::error_code; namespace detail { -struct Listener { - std::string topic; - bool authed; - bool persistent; - bool confirmed = false; -}; + struct Listener { + std::string topic; + bool authed; + bool persistent; + bool confirmed = false; + }; -class PubSubClient : public std::enable_shared_from_this -{ -public: - PubSubClient(WebsocketClient &_websocketClient, WebsocketHandle _handle); + class PubSubClient : public std::enable_shared_from_this + { + public: + PubSubClient(WebsocketClient &_websocketClient, + WebsocketHandle _handle); - void start(); - void stop(); + void start(); + void stop(); - bool listen(rapidjson::Document &message); - void unlistenPrefix(const std::string &prefix); + bool listen(rapidjson::Document &message); + void unlistenPrefix(const std::string &prefix); - void handlePong(); + void handlePong(); - bool isListeningToTopic(const std::string &topic); + bool isListeningToTopic(const std::string &topic); -private: - void ping(); - bool send(const char *payload); + private: + void ping(); + bool send(const char *payload); - WebsocketClient &websocketClient_; - WebsocketHandle handle_; - uint16_t numListens_ = 0; + WebsocketClient &websocketClient_; + WebsocketHandle handle_; + uint16_t numListens_ = 0; - std::vector listeners_; + std::vector listeners_; - std::atomic awaitingPong_{false}; - std::atomic started_{false}; -}; + std::atomic awaitingPong_{false}; + std::atomic started_{false}; + }; } // namespace detail diff --git a/src/providers/twitch/PubsubHelpers.cpp b/src/providers/twitch/PubsubHelpers.cpp index cf4560f6b..f9b4e784a 100644 --- a/src/providers/twitch/PubsubHelpers.cpp +++ b/src/providers/twitch/PubsubHelpers.cpp @@ -1,6 +1,7 @@ #include "providers/twitch/PubsubHelpers.hpp" #include "providers/twitch/PubsubActions.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { diff --git a/src/providers/twitch/PubsubHelpers.hpp b/src/providers/twitch/PubsubHelpers.hpp index a891edd4f..c478ed437 100644 --- a/src/providers/twitch/PubsubHelpers.hpp +++ b/src/providers/twitch/PubsubHelpers.hpp @@ -1,16 +1,14 @@ #pragma once -#include "debug/Log.hpp" -#include "providers/twitch/TwitchAccount.hpp" -#include "util/RapidjsonHelpers.hpp" - #include #include - #include +#include "debug/Log.hpp" +#include "util/RapidjsonHelpers.hpp" namespace chatterino { +class TwitchAccount; struct ActionUser; const rapidjson::Value &getArgs(const rapidjson::Value &data); @@ -35,7 +33,7 @@ void runAfter(boost::asio::io_service &ioService, Duration duration, timer->async_wait([timer, cb](const boost::system::error_code &ec) { if (ec) { - Log("Error in runAfter: {}", ec.message()); + log("Error in runAfter: {}", ec.message()); return; } @@ -52,7 +50,7 @@ void runAfter(std::shared_ptr timer, timer->async_wait([timer, cb](const boost::system::error_code &ec) { if (ec) { - Log("Error in runAfter: {}", ec.message()); + log("Error in runAfter: {}", ec.message()); return; } diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 0f8b79a44..3e7ea237d 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -4,6 +4,7 @@ #include "Application.hpp" #include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" #include "debug/Log.hpp" #include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -14,32 +15,32 @@ namespace chatterino { namespace { -EmoteName cleanUpCode(const EmoteName &dirtyEmoteCode) -{ - auto cleanCode = dirtyEmoteCode.string; - cleanCode.detach(); + EmoteName cleanUpCode(const EmoteName &dirtyEmoteCode) + { + auto cleanCode = dirtyEmoteCode.string; + cleanCode.detach(); - static QMap emoteNameReplacements{ - {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, - {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, - {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, - {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, - {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, - {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, - {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, - }; + static QMap emoteNameReplacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, + {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, + {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, + {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, + {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, + }; - auto it = emoteNameReplacements.find(dirtyEmoteCode.string); - if (it != emoteNameReplacements.end()) { - cleanCode = it.value(); + auto it = emoteNameReplacements.find(dirtyEmoteCode.string); + if (it != emoteNameReplacements.end()) { + cleanCode = it.value(); + } + + cleanCode.replace("<", "<"); + cleanCode.replace(">", ">"); + + return {cleanCode}; } - cleanCode.replace("<", "<"); - cleanCode.replace(">", ">"); - - return {cleanCode}; -} - } // namespace TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, @@ -78,6 +79,16 @@ const QString &TwitchAccount::getUserId() const return this->userId_; } +QColor TwitchAccount::color() +{ + return this->color_.get(); +} + +void TwitchAccount::setColor(QColor color) +{ + this->color_.set(color); +} + bool TwitchAccount::setOAuthClient(const QString &newClientID) { if (this->oauthClient_.compare(newClientID) == 0) { @@ -143,7 +154,7 @@ void TwitchAccount::loadIgnores() } TwitchUser ignoredUser; if (!rj::getSafe(userIt->value, ignoredUser)) { - Log("Error parsing twitch user JSON {}", + log("Error parsing twitch user JSON {}", rj::stringify(userIt->value)); continue; } @@ -368,13 +379,13 @@ std::set TwitchAccount::getIgnores() const void TwitchAccount::loadEmotes() { - Log("Loading Twitch emotes for user {}", this->getUserName()); + log("Loading Twitch emotes for user {}", this->getUserName()); const auto &clientID = this->getOAuthClient(); const auto &oauthToken = this->getOAuthToken(); if (clientID.isEmpty() || oauthToken.isEmpty()) { - Log("Missing Client ID or OAuth token"); + log("Missing Client ID or OAuth token"); return; } @@ -386,7 +397,7 @@ void TwitchAccount::loadEmotes() req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); req.onError([=](int errorCode) { - Log("[TwitchAccount::loadEmotes] Error {}", errorCode); + log("[TwitchAccount::loadEmotes] Error {}", errorCode); if (errorCode == 203) { // onFinished(FollowResult_NotFollowing); } else { @@ -406,7 +417,7 @@ void TwitchAccount::loadEmotes() } AccessGuard -TwitchAccount::accessEmotes() const + TwitchAccount::accessEmotes() const { return this->emotes_.accessConst(); } @@ -420,7 +431,7 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root) auto emoticonSets = root.FindMember("emoticon_sets"); if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject()) { - Log("No emoticon_sets in load emotes response"); + log("No emoticon_sets in load emotes response"); return; } @@ -434,19 +445,19 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root) for (const rapidjson::Value &emoteJSON : emoteSetJSON.value.GetArray()) { if (!emoteJSON.IsObject()) { - Log("Emote value was invalid"); + log("Emote value was invalid"); return; } uint64_t idNumber; if (!rj::getSafe(emoteJSON, "id", idNumber)) { - Log("No ID key found in Emote value"); + log("No ID key found in Emote value"); return; } QString _code; if (!rj::getSafe(emoteJSON, "code", _code)) { - Log("No code key found in Emote value"); + log("No code key found in Emote value"); return; } @@ -468,7 +479,7 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root) void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) { if (!emoteSet) { - Log("null emote set sent"); + log("null emote set sent"); return; } @@ -486,7 +497,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) req.setUseQuickLoadCache(true); req.onError([](int errorCode) -> bool { - Log("Error code {} while loading emote set data", errorCode); + log("Error code {} while loading emote set data", errorCode); return true; }); @@ -507,16 +518,15 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) return Failure; } - Log("Loaded twitch emote set data for {}!", emoteSet->key); + log("Loaded twitch emote set data for {}!", emoteSet->key); - if (type == "sub") { - emoteSet->text = - QString("Twitch Subscriber Emote (%1)").arg(channelName); - } else { - emoteSet->text = - QString("Twitch Account Emote (%1)").arg(channelName); - } + auto name = channelName; + name.detach(); + name[0] = name[0].toUpper(); + emoteSet->text = name; + + emoteSet->type = type; emoteSet->channelName = channelName; return Success; diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index 1a180f5e2..1ea718b73 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -1,5 +1,7 @@ #pragma once +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" #include "common/UniqueAccess.hpp" #include "controllers/accounts/Account.hpp" #include "messages/Emote.hpp" @@ -44,6 +46,7 @@ public: QString key; QString channelName; QString text; + QString type; std::vector emotes; }; @@ -65,9 +68,11 @@ public: const QString &getUserName() const; const QString &getOAuthToken() const; const QString &getOAuthClient() const; - const QString &getUserId() const; + QColor color(); + void setColor(QColor color); + // Attempts to update the users OAuth Client ID // Returns true if the value has changed, otherwise false bool setOAuthClient(const QString &newClientID); @@ -103,8 +108,6 @@ public: void loadEmotes(); AccessGuard accessEmotes() const; - QColor color; - private: void parseEmotes(const rapidjson::Document &document); void loadEmoteSetData(std::shared_ptr emoteSet); @@ -114,6 +117,7 @@ private: QString userName_; QString userId_; const bool isAnon_; + Atomic color_; mutable std::mutex ignoresMutex_; std::set ignores_; diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 7d4f932eb..097aa89ea 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -2,6 +2,7 @@ #include "common/Common.hpp" #include "debug/Log.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" namespace chatterino { @@ -93,20 +94,20 @@ void TwitchAccountManager::reloadUsers() switch (this->addUser(userData)) { case AddUserResponse::UserAlreadyExists: { - Log("User {} already exists", userData.username); + log("User {} already exists", userData.username); // Do nothing } break; case AddUserResponse::UserValuesUpdated: { - Log("User {} already exists, and values updated!", + log("User {} already exists, and values updated!", userData.username); if (userData.username == this->getCurrent()->getUserName()) { - Log("It was the current user, so we need to reconnect " + log("It was the current user, so we need to reconnect " "stuff!"); this->currentUserChanged.invoke(); } } break; case AddUserResponse::UserAdded: { - Log("Added user {}", userData.username); + log("Added user {}", userData.username); listUpdated = true; } break; } @@ -125,12 +126,12 @@ void TwitchAccountManager::load() QString newUsername(QString::fromStdString(newValue)); auto user = this->findUserByUsername(newUsername); if (user) { - Log("[AccountManager:currentUsernameChanged] User successfully " + log("[AccountManager:currentUsernameChanged] User successfully " "updated to {}", newUsername); this->currentUser_ = user; } else { - Log("[AccountManager:currentUsernameChanged] User successfully " + log("[AccountManager:currentUsernameChanged] User successfully " "updated to anonymous"); this->currentUser_ = this->anonymousUser_; } diff --git a/src/providers/twitch/TwitchAccountManager.hpp b/src/providers/twitch/TwitchAccountManager.hpp index 32961f092..7c79ea249 100644 --- a/src/providers/twitch/TwitchAccountManager.hpp +++ b/src/providers/twitch/TwitchAccountManager.hpp @@ -17,6 +17,7 @@ namespace chatterino { +class TwitchAccount; class AccountController; class TwitchAccountManager diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp index 6b1e537c3..33e982191 100644 --- a/src/providers/twitch/TwitchApi.cpp +++ b/src/providers/twitch/TwitchApi.cpp @@ -1,5 +1,6 @@ #include "providers/twitch/TwitchApi.hpp" +#include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -20,25 +21,25 @@ void TwitchApi::findUserId(const QString user, request.onSuccess([successCallback](auto result) mutable -> Outcome { auto root = result.parseJson(); if (!root.value("users").isArray()) { - Log("API Error while getting user id, users is not an array"); + log("API Error while getting user id, users is not an array"); successCallback(""); return Failure; } auto users = root.value("users").toArray(); if (users.size() != 1) { - Log("API Error while getting user id, users array size is not 1"); + log("API Error while getting user id, users array size is not 1"); successCallback(""); return Failure; } if (!users[0].isObject()) { - Log("API Error while getting user id, first user is not an object"); + log("API Error while getting user id, first user is not an object"); successCallback(""); return Failure; } auto firstUser = users[0].toObject(); auto id = firstUser.value("_id"); if (!id.isString()) { - Log("API Error: while getting user id, first user object `_id` key " + log("API Error: while getting user id, first user object `_id` key " "is not a " "string"); successCallback(""); diff --git a/src/providers/twitch/TwitchApi.hpp b/src/providers/twitch/TwitchApi.hpp index 67b26c1ca..fd7ad599c 100644 --- a/src/providers/twitch/TwitchApi.hpp +++ b/src/providers/twitch/TwitchApi.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace chatterino { diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index fd9fe7461..0e515d627 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -4,19 +4,14 @@ #include #include #include + #include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" +#include "debug/Log.hpp" +#include "messages/Emote.hpp" namespace chatterino { -TwitchBadges::TwitchBadges() -{ -} - -void TwitchBadges::initialize(Settings &settings, Paths &paths) -{ - this->loadTwitchBadges(); -} - void TwitchBadges::loadTwitchBadges() { static QString url( @@ -26,32 +21,34 @@ void TwitchBadges::loadTwitchBadges() req.setCaller(QThread::currentThread()); req.onSuccess([this](auto result) -> Outcome { auto root = result.parseJson(); - QJsonObject sets = root.value("badge_sets").toObject(); + auto badgeSets = this->badgeSets_.access(); - for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { - QJsonObject versions = - it.value().toObject().value("versions").toObject(); + auto jsonSets = root.value("badge_sets").toObject(); + for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt) { + auto key = sIt.key(); + auto versions = sIt.value().toObject().value("versions").toObject(); + + for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt) { + auto versionObj = vIt.value().toObject(); - for (auto versionIt = std::begin(versions); - versionIt != std::end(versions); ++versionIt) { auto emote = Emote{ {""}, ImageSet{ - Image::fromUrl({root.value("image_url_1x").toString()}, - 1), - Image::fromUrl({root.value("image_url_2x").toString()}, - 0.5), - Image::fromUrl({root.value("image_url_4x").toString()}, - 0.25), + Image::fromUrl( + {versionObj.value("image_url_1x").toString()}, 1), + Image::fromUrl( + {versionObj.value("image_url_2x").toString()}, .5), + Image::fromUrl( + {versionObj.value("image_url_4x").toString()}, .25), }, Tooltip{root.value("description").toString()}, Url{root.value("clickURL").toString()}}; // "title" // "clickAction" - QJsonObject versionObj = versionIt.value().toObject(); - this->badges.emplace(versionIt.key(), - std::make_shared(emote)); + log("{} {}", key, vIt.key()); + + (*badgeSets)[key][vIt.key()] = std::make_shared(emote); } } @@ -61,4 +58,18 @@ void TwitchBadges::loadTwitchBadges() req.execute(); } +boost::optional TwitchBadges::badge(const QString &set, + const QString &version) const +{ + auto badgeSets = this->badgeSets_.access(); + auto it = badgeSets->find(set); + if (it != badgeSets->end()) { + auto it2 = it->second.find(version); + if (it2 != it->second.end()) { + return it2->second; + } + } + return boost::none; +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp index a228365da..d9023855d 100644 --- a/src/providers/twitch/TwitchBadges.hpp +++ b/src/providers/twitch/TwitchBadges.hpp @@ -1,26 +1,32 @@ #pragma once #include -#include +#include #include + +#include "common/UniqueAccess.hpp" #include "util/QStringHash.hpp" namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + class Settings; class Paths; class TwitchBadges { public: - TwitchBadges(); - - void initialize(Settings &settings, Paths &paths); - -private: void loadTwitchBadges(); - std::unordered_map badges; + boost::optional badge(const QString &set, + const QString &version) const; + +private: + UniqueAccess< + std::unordered_map>> + badgeSets_; // "bits": { "100": ... "500": ... }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 6915372c7..674c5831a 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1,11 +1,13 @@ #include "providers/twitch/TwitchChannel.hpp" +#include "Application.hpp" #include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/notifications/NotificationController.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" +#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/LoadBttvChannelEmote.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -25,18 +27,67 @@ #include namespace chatterino { +namespace { + auto parseRecentMessages(const QJsonObject &jsonRoot, + TwitchChannel &channel) + { + QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); + std::vector messages; -TwitchChannel::TwitchChannel(const QString &name) + if (jsonMessages.empty()) return messages; + + for (const auto jsonMessage : jsonMessages) { + auto content = jsonMessage.toString().toUtf8(); + // passing nullptr as the channel makes the message invalid but we + // don't check for that anyways + auto message = Communi::IrcMessage::fromData(content, nullptr); + auto privMsg = dynamic_cast(message); + assert(privMsg); + + MessageParseArgs args; + TwitchMessageBuilder builder(&channel, privMsg, args); + if (!builder.isIgnored()) { + messages.push_back(builder.build()); + } + } + + return messages; + } + std::pair parseChatters(const QJsonObject &jsonRoot) + { + static QStringList categories = {"moderators", "staff", "admins", + "global_mods", "viewers"}; + + auto usernames = UsernameSet(); + + // parse json + QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); + + for (const auto &category : categories) { + for (auto jsonCategory : jsonCategories.value(category).toArray()) { + usernames.insert(jsonCategory.toString()); + } + } + + return {Success, std::move(usernames)}; + } +} // namespace + +TwitchChannel::TwitchChannel(const QString &name, + TwitchBadges &globalTwitchBadges, BttvEmotes &bttv, + FfzEmotes &ffz) : Channel(name, Channel::Type::Twitch) , subscriptionUrl_("https://www.twitch.tv/subs/" + name) , channelUrl_("https://twitch.tv/" + name) , popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name) + , globalTwitchBadges_(globalTwitchBadges) + , globalBttv_(bttv) + , globalFfz_(ffz) + , bttvEmotes_(std::make_shared()) + , ffzEmotes_(std::make_shared()) , mod_(false) { - Log("[TwitchChannel:{}] Opened", name); - - // this->refreshChannelEmotes(); - // this->refreshViewerList(); + log("[TwitchChannel:{}] Opened", name); this->tabHighlightRequested.connect([](HighlightState state) {}); this->liveStatusChanged.connect([this]() { @@ -48,22 +99,22 @@ TwitchChannel::TwitchChannel(const QString &name) [=] { this->setMod(false); }); // pubsub - this->userStateChanged.connect([=] { this->refreshPubsub(); }); this->managedConnect(getApp()->accounts->twitch.currentUserChanged, [=] { this->refreshPubsub(); }); this->refreshPubsub(); + this->userStateChanged.connect([this] { this->refreshPubsub(); }); // room id loaded -> refresh live status this->roomIdChanged.connect([this]() { this->refreshPubsub(); this->refreshLiveStatus(); - this->loadBadges(); - this->loadCheerEmotes(); + this->refreshBadges(); + this->refreshCheerEmotes(); }); // timers QObject::connect(&this->chattersListTimer_, &QTimer::timeout, - [=] { this->refreshViewerList(); }); + [=] { this->refreshChatters(); }); this->chattersListTimer_.start(5 * 60 * 1000); QObject::connect(&this->liveStatusTimer_, &QTimer::timeout, @@ -77,11 +128,17 @@ TwitchChannel::TwitchChannel(const QString &name) // debugging #if 0 for (int i = 0; i < 1000; i++) { - this->addMessage(makeSystemMessage("asdf")); + this->addMessage(makeSystemMessage("asef")); } #endif } +void TwitchChannel::initialize() +{ + this->refreshChatters(); + this->refreshChannelEmotes(); +} + bool TwitchChannel::isEmpty() const { return this->getName().isEmpty(); @@ -94,15 +151,17 @@ bool TwitchChannel::canSendMessage() const void TwitchChannel::refreshChannelEmotes() { - loadBttvChannelEmotes( - this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { - if (auto shared = weak.lock()) // - *this->bttvEmotes_.access() = emoteMap; - }); - getApp()->emotes->ffz.loadChannelEmotes( + BttvEmotes::loadChannel( this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { if (auto shared = weak.lock()) - *this->ffzEmotes_.access() = emoteMap; + this->bttvEmotes_.set( + std::make_shared(std::move(emoteMap))); + }); + FfzEmotes::loadChannel( + this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) + this->ffzEmotes_.set( + std::make_shared(std::move(emoteMap))); }); } @@ -119,7 +178,7 @@ void TwitchChannel::sendMessage(const QString &message) return; } - Log("[TwitchChannel:{}] Send message: {}", this->getName(), message); + log("[TwitchChannel:{}] Send message: {}", this->getName(), message); // Do last message processing QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); @@ -170,9 +229,7 @@ bool TwitchChannel::isBroadcaster() const void TwitchChannel::addRecentChatter(const MessagePtr &message) { - assert(!message->loginName.isEmpty()); - - this->completionModel.addUser(message->displayName); + this->chatters_.access()->insert(message->displayName); } void TwitchChannel::addJoinedUser(const QString &user) @@ -231,14 +288,14 @@ void TwitchChannel::addPartedUser(const QString &user) } } -QString TwitchChannel::getRoomId() const +QString TwitchChannel::roomId() const { - return this->roomID_.get(); + return *this->roomID_.access(); } void TwitchChannel::setRoomId(const QString &id) { - this->roomID_.set(id); + (*this->roomID_.access()) = id; this->roomIdChanged.invoke(); this->loadRecentMessages(); } @@ -262,52 +319,70 @@ bool TwitchChannel::isLive() const } AccessGuard -TwitchChannel::accessStreamStatus() const + TwitchChannel::accessStreamStatus() const { return this->streamStatus_.accessConst(); } -boost::optional TwitchChannel::getBttvEmote( - const EmoteName &name) const +AccessGuard TwitchChannel::accessChatters() const { - auto emotes = this->bttvEmotes_.access(); + return this->chatters_.accessConst(); +} + +const TwitchBadges &TwitchChannel::globalTwitchBadges() const +{ + return this->globalTwitchBadges_; +} + +const BttvEmotes &TwitchChannel::globalBttv() const +{ + return this->globalBttv_; +} + +const FfzEmotes &TwitchChannel::globalFfz() const +{ + return this->globalFfz_; +} + +boost::optional TwitchChannel::bttvEmote(const EmoteName &name) const +{ + auto emotes = this->bttvEmotes_.get(); auto it = emotes->find(name); if (it == emotes->end()) return boost::none; return it->second; } -boost::optional TwitchChannel::getFfzEmote( - const EmoteName &name) const +boost::optional TwitchChannel::ffzEmote(const EmoteName &name) const { - auto emotes = this->bttvEmotes_.access(); + auto emotes = this->ffzEmotes_.get(); auto it = emotes->find(name); if (it == emotes->end()) return boost::none; return it->second; } -AccessGuard TwitchChannel::accessBttvEmotes() const +std::shared_ptr TwitchChannel::bttvEmotes() const { - return this->bttvEmotes_.accessConst(); + return this->bttvEmotes_.get(); } -AccessGuard TwitchChannel::accessFfzEmotes() const +std::shared_ptr TwitchChannel::ffzEmotes() const { - return this->ffzEmotes_.accessConst(); + return this->ffzEmotes_.get(); } -const QString &TwitchChannel::getSubscriptionUrl() +const QString &TwitchChannel::subscriptionUrl() { return this->subscriptionUrl_; } -const QString &TwitchChannel::getChannelUrl() +const QString &TwitchChannel::channelUrl() { return this->channelUrl_; } -const QString &TwitchChannel::getPopoutPlayerUrl() +const QString &TwitchChannel::popoutPlayerUrl() { return this->popoutPlayerUrl_; } @@ -347,16 +422,16 @@ void TwitchChannel::setLive(bool newLiveStatus) void TwitchChannel::refreshLiveStatus() { - auto roomID = this->getRoomId(); + auto roomID = this->roomId(); if (roomID.isEmpty()) { - Log("[TwitchChannel:{}] Refreshing live status (Missing ID)", + log("[TwitchChannel:{}] Refreshing live status (Missing ID)", this->getName()); this->setLive(false); return; } - Log("[TwitchChannel:{}] Refreshing live status", this->getName()); + log("[TwitchChannel:{}] Refreshing live status", this->getName()); QString url("https://api.twitch.tv/kraken/streams/" + roomID); @@ -368,7 +443,7 @@ void TwitchChannel::refreshLiveStatus() //>>>>>>> 9bfbdefd2f0972a738230d5b95a009f73b1dd933 request.onSuccess( - [this, weak = this->weak_from_this()](auto result) -> Outcome { + [this, weak = weakOf(this)](auto result) -> Outcome { ChannelPtr shared = weak.lock(); if (!shared) return Failure; @@ -381,12 +456,12 @@ void TwitchChannel::refreshLiveStatus() Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) { if (!document.IsObject()) { - Log("[TwitchChannel:refreshLiveStatus] root is not an object"); + log("[TwitchChannel:refreshLiveStatus] root is not an object"); return Failure; } if (!document.HasMember("stream")) { - Log("[TwitchChannel:refreshLiveStatus] Missing stream in root"); + log("[TwitchChannel:refreshLiveStatus] Missing stream in root"); return Failure; } @@ -400,7 +475,7 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) if (!stream.HasMember("viewers") || !stream.HasMember("game") || !stream.HasMember("channel") || !stream.HasMember("created_at")) { - Log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); + log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); this->setLive(false); return Failure; } @@ -408,7 +483,7 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) const rapidjson::Value &streamChannel = stream["channel"]; if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) { - Log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in " + log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in " "channel"); return Failure; } @@ -458,53 +533,31 @@ void TwitchChannel::loadRecentMessages() "https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + getDefaultClientID(); - NetworkRequest request(genericURL.arg(this->getRoomId())); + NetworkRequest request(genericURL.arg(this->roomId())); request.makeAuthorizedV5(getDefaultClientID()); request.setCaller(QThread::currentThread()); + // can't be concurrent right now due to SignalVector + // request.setExecuteConcurrently(true); - request.onSuccess( - [this, weak = weakOf(this)](auto result) -> Outcome { - ChannelPtr shared = weak.lock(); - if (!shared) return Failure; + request.onSuccess([that = this](auto result) -> Outcome { + auto messages = parseRecentMessages(result.parseJson(), *that); - return this->parseRecentMessages(result.parseJson()); - }); + // postToThread([that, weak = weakOf(that), + // messages = std::move(messages)]() mutable { + that->addMessagesAtStart(messages); + // }); + + return Success; + }); request.execute(); } -Outcome TwitchChannel::parseRecentMessages(const QJsonObject &jsonRoot) -{ - QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); - if (jsonMessages.empty()) return Failure; - - std::vector messages; - - for (const auto jsonMessage : jsonMessages) { - auto content = jsonMessage.toString().toUtf8(); - // passing nullptr as the channel makes the message invalid but we don't - // check for that anyways - auto message = Communi::IrcMessage::fromData(content, nullptr); - auto privMsg = dynamic_cast(message); - assert(privMsg); - - MessageParseArgs args; - TwitchMessageBuilder builder(this, privMsg, args); - if (!builder.isIgnored()) { - messages.push_back(builder.build()); - } - } - - this->addMessagesAtStart(messages); - - return Success; -} - void TwitchChannel::refreshPubsub() { // listen to moderation actions if (!this->hasModRights()) return; - auto roomId = this->getRoomId(); + auto roomId = this->roomId(); if (roomId.isEmpty()) return; auto account = getApp()->accounts->twitch.getCurrent(); @@ -512,7 +565,7 @@ void TwitchChannel::refreshPubsub() account); } -void TwitchChannel::refreshViewerList() +void TwitchChannel::refreshChatters() { // setting? const auto streamStatus = this->accessStreamStatus(); @@ -530,39 +583,26 @@ void TwitchChannel::refreshViewerList() request.setCaller(QThread::currentThread()); request.onSuccess( - [this, weak = this->weak_from_this()](auto result) -> Outcome { + [this, weak = weakOf(this)](auto result) -> Outcome { // channel still exists? auto shared = weak.lock(); if (!shared) return Failure; - return this->parseViewerList(result.parseJson()); + auto pair = parseChatters(result.parseJson()); + if (pair.first) { + *this->chatters_.access() = std::move(pair.second); + } + + return pair.first; }); request.execute(); } -Outcome TwitchChannel::parseViewerList(const QJsonObject &jsonRoot) -{ - static QStringList categories = {"moderators", "staff", "admins", - "global_mods", "viewers"}; - - // parse json - QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); - - for (const auto &category : categories) { - for (const auto jsonCategory : - jsonCategories.value(category).toArray()) { - this->completionModel.addUser(jsonCategory.toString()); - } - } - - return Success; -} - -void TwitchChannel::loadBadges() +void TwitchChannel::refreshBadges() { auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" + - this->getRoomId() + "/display?language=en"}; + this->roomId() + "/display?language=en"}; NetworkRequest req(url.string); req.setCaller(QThread::currentThread()); @@ -604,9 +644,9 @@ void TwitchChannel::loadBadges() req.execute(); } -void TwitchChannel::loadCheerEmotes() +void TwitchChannel::refreshCheerEmotes() { - auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" + + /*auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" + this->getRoomId()}; auto request = NetworkRequest::twitchRequest(url.string); request.setCaller(QThread::currentThread()); @@ -614,6 +654,7 @@ void TwitchChannel::loadCheerEmotes() request.onSuccess( [this, weak = weakOf(this)](auto result) -> Outcome { auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson()); + std::vector emoteSets; for (auto &set : cheerEmoteSets) { auto cheerEmoteSet = CheerEmoteSet(); @@ -656,16 +697,18 @@ void TwitchChannel::loadCheerEmotes() return lhs.minBits < rhs.minBits; // }); - this->cheerEmoteSets_.emplace_back(cheerEmoteSet); + emoteSets.emplace_back(cheerEmoteSet); } + *this->cheerEmoteSets_.access() = std::move(emoteSets); return Success; }); request.execute(); + */ } -boost::optional TwitchChannel::getTwitchBadge( +boost::optional TwitchChannel::twitchBadge( const QString &set, const QString &version) const { auto badgeSets = this->badgeSets_.access(); diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index ec690aefd..208a80e5b 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -1,22 +1,32 @@ #pragma once -#include - +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" #include "common/Channel.hpp" -#include "common/Common.hpp" -#include "common/MutexValue.hpp" +#include "common/Outcome.hpp" #include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" -#include "singletons/Emotes.hpp" -#include "util/ConcurrentMap.hpp" - -#include +#include "common/UsernameSet.hpp" +#include "providers/twitch/TwitchEmotes.hpp" +#include +#include +#include +#include +#include #include +#include #include namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; +class EmoteMap; + +class TwitchBadges; +class FfzEmotes; +class BttvEmotes; + class TwitchServer; class TwitchChannel final : public Channel, pajlada::Signals::SignalHolder @@ -32,11 +42,6 @@ public: QString streamType; }; - struct UserState { - bool mod; - bool broadcaster; - }; - struct RoomModes { bool submode = false; bool r9k = false; @@ -46,107 +51,104 @@ public: QString broadcasterLang; }; - void refreshChannelEmotes(); + void initialize(); // Channel methods virtual bool isEmpty() const override; virtual bool canSendMessage() const override; virtual void sendMessage(const QString &message) override; - - // Auto completion - void addRecentChatter(const MessagePtr &message) final; - void addJoinedUser(const QString &user); - void addPartedUser(const QString &user); - - // Twitch data - bool isLive() const; virtual bool isMod() const override; - void setMod(bool value); virtual bool isBroadcaster() const override; - QString getRoomId() const; - void setRoomId(const QString &id); + // Data + const QString &subscriptionUrl(); + const QString &channelUrl(); + const QString &popoutPlayerUrl(); + bool isLive() const; + QString roomId() const; AccessGuard accessRoomModes() const; - void setRoomModes(const RoomModes &roomModes_); AccessGuard accessStreamStatus() const; + AccessGuard accessChatters() const; - boost::optional getBttvEmote(const EmoteName &name) const; - boost::optional getFfzEmote(const EmoteName &name) const; - AccessGuard accessBttvEmotes() const; - AccessGuard accessFfzEmotes() const; - const QString &getSubscriptionUrl(); - const QString &getChannelUrl(); - const QString &getPopoutPlayerUrl(); + // Emotes + const TwitchBadges &globalTwitchBadges() const; + const BttvEmotes &globalBttv() const; + const FfzEmotes &globalFfz() const; + boost::optional bttvEmote(const EmoteName &name) const; + boost::optional ffzEmote(const EmoteName &name) const; + std::shared_ptr bttvEmotes() const; + std::shared_ptr ffzEmotes() const; - boost::optional getTwitchBadge(const QString &set, - const QString &version) const; + void refreshChannelEmotes(); + + // Badges + boost::optional twitchBadge(const QString &set, + const QString &version) const; // Signals pajlada::Signals::NoArgSignal roomIdChanged; - pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal userStateChanged; + pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal roomModesChanged; pajlada::Signals::Signal tabHighlightRequested; +protected: + void addRecentChatter(const MessagePtr &message) override; + private: struct NameOptions { QString displayName; QString localizedName; }; - struct CheerEmote { - // a Cheermote indicates one tier - QColor color; - int minBits; - - EmotePtr animatedEmote; - EmotePtr staticEmote; - }; - - struct CheerEmoteSet { - QRegularExpression regex; - std::vector cheerEmotes; - }; - - explicit TwitchChannel(const QString &channelName); + explicit TwitchChannel(const QString &channelName, + TwitchBadges &globalTwitchBadges, + BttvEmotes &globalBttv, FfzEmotes &globalFfz); // Methods void refreshLiveStatus(); Outcome parseLiveStatus(const rapidjson::Document &document); void refreshPubsub(); - void refreshViewerList(); - Outcome parseViewerList(const QJsonObject &jsonRoot); + void refreshChatters(); + void refreshBadges(); + void refreshCheerEmotes(); void loadRecentMessages(); - Outcome parseRecentMessages(const QJsonObject &jsonRoot); + void addJoinedUser(const QString &user); + void addPartedUser(const QString &user); void setLive(bool newLiveStatus); + void setMod(bool value); + void setRoomId(const QString &id); + void setRoomModes(const RoomModes &roomModes_); - void loadBadges(); - void loadCheerEmotes(); - - // Twitch data - UniqueAccess streamStatus_; - UniqueAccess userState_; - UniqueAccess roomModes_; - - UniqueAccess bttvEmotes_; - UniqueAccess ffzEmotes_; + // Data const QString subscriptionUrl_; const QString channelUrl_; const QString popoutPlayerUrl_; + UniqueAccess streamStatus_; + UniqueAccess roomModes_; + UniqueAccess chatters_; // maps 2 char prefix to set of names + + // Emotes + TwitchBadges &globalTwitchBadges_; + BttvEmotes &globalBttv_; + FfzEmotes &globalFfz_; + Atomic> bttvEmotes_; + Atomic> ffzEmotes_; + + // Badges + UniqueAccess>> + badgeSets_; // "subscribers": { "0": ... "3": ... "6": ... + UniqueAccess> cheerEmoteSets_; bool mod_ = false; - MutexValue roomID_; + UniqueAccess roomID_; UniqueAccess joinedUsers_; bool joinedUsersMergeQueued_ = false; UniqueAccess partedUsers_; bool partedUsersMergeQueued_ = false; - // "subscribers": { "0": ... "3": ... "6": ... - UniqueAccess>> badgeSets_; - std::vector cheerEmoteSets_; - // -- QByteArray messageSuffix_; QString lastSentMessage_; @@ -155,6 +157,8 @@ private: QTimer chattersListTimer_; friend class TwitchServer; + friend class TwitchMessageBuilder; + friend class IrcMessageHandler; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 08c28286f..ce63d6864 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -3,6 +3,7 @@ #include "common/NetworkRequest.hpp" #include "debug/Benchmark.hpp" #include "debug/Log.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index 665e1e9cd..1c1b0b216 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -1,20 +1,33 @@ #pragma once +#include +#include #include #include -#include "common/Emotemap.hpp" +#include "common/Aliases.hpp" #include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" -#include "providers/twitch/EmoteValue.hpp" -#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchEmotes.hpp" -#include "util/ConcurrentMap.hpp" #define TWITCH_EMOTE_TEMPLATE \ "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + +struct CheerEmote { + QColor color; + int minBits; + + EmotePtr animatedEmote; + EmotePtr staticEmote; +}; + +struct CheerEmoteSet { + QRegularExpression regex; + std::vector cheerEmotes; +}; class TwitchEmotes { diff --git a/src/providers/twitch/TwitchHelpers.cpp b/src/providers/twitch/TwitchHelpers.cpp index f236b10cb..9999e125c 100644 --- a/src/providers/twitch/TwitchHelpers.cpp +++ b/src/providers/twitch/TwitchHelpers.cpp @@ -6,7 +6,7 @@ namespace chatterino { bool trimChannelName(const QString &channelName, QString &outChannelName) { if (channelName.length() < 3) { - Log("channel name length below 3"); + log("channel name length below 3"); return false; } diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 8eafa7dd6..a6647d88e 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -5,6 +5,8 @@ #include "controllers/highlights/HighlightController.hpp" #include "controllers/ignores/IgnoreController.hpp" #include "debug/Log.hpp" +#include "messages/Message.hpp" +#include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" @@ -12,6 +14,7 @@ #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/IrcHelpers.hpp" +#include "widgets/Window.hpp" #include #include @@ -31,8 +34,7 @@ TwitchMessageBuilder::TwitchMessageBuilder( , originalMessage_(_ircMessage->content()) , action_(_ircMessage->isAction()) { - auto app = getApp(); - this->usernameColor_ = app->themes->messages.textColors.system; + this->usernameColor_ = getApp()->themes->messages.textColors.system; } TwitchMessageBuilder::TwitchMessageBuilder( @@ -46,8 +48,7 @@ TwitchMessageBuilder::TwitchMessageBuilder( , originalMessage_(content) , action_(isAction) { - auto app = getApp(); - this->usernameColor_ = app->themes->messages.textColors.system; + this->usernameColor_ = getApp()->themes->messages.textColors.system; } bool TwitchMessageBuilder::isIgnored() const @@ -57,20 +58,20 @@ bool TwitchMessageBuilder::isIgnored() const // TODO(pajlada): Do we need to check if the phrase is valid first? for (const auto &phrase : app->ignores->phrases.getVector()) { if (phrase.isMatch(this->originalMessage_)) { - Log("Blocking message because it contains ignored phrase {}", + log("Blocking message because it contains ignored phrase {}", phrase.getPattern()); return true; } } - if (app->settings->enableTwitchIgnoredUsers && + if (getSettings()->enableTwitchIgnoredUsers && this->tags.contains("user-id")) { auto sourceUserID = this->tags.value("user-id").toString(); for (const auto &user : app->accounts->twitch.getCurrent()->getIgnores()) { if (sourceUserID == user.id) { - Log("Blocking message because it's from blocked user {}", + log("Blocking message because it's from blocked user {}", user.name); return true; } @@ -82,8 +83,6 @@ bool TwitchMessageBuilder::isIgnored() const MessagePtr TwitchMessageBuilder::build() { - auto app = getApp(); - // PARSING this->parseUsername(); @@ -91,13 +90,6 @@ MessagePtr TwitchMessageBuilder::build() this->senderIsBroadcaster = true; } - //#ifdef XD - // if (this->originalMessage.length() > 100) { - // this->message->flags.has(MessageFlag::Collapsed); - // this->emplace(getApp()->resources->badgeCollapsed, - // MessageElementFlag::Collapsed); - // } - //#endif this->message().flags.has(MessageFlag::Collapsed); // PARSING @@ -203,11 +195,7 @@ void TwitchMessageBuilder::addWords( // split words for (auto &variant : getApp()->emotes->emojis.parse(word)) { - boost::apply_visitor(/*overloaded{[&](EmotePtr arg) { - this->addTextOrEmoji(arg); }, - [&](const QString &arg) { - this->addTextOrEmoji(arg); }}*/ - [&](auto &&arg) { this->addTextOrEmoji(arg); }, + boost::apply_visitor([&](auto &&arg) { this->addTextOrEmoji(arg); }, variant); } @@ -289,7 +277,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } // if (!linkString.isEmpty()) { - // if (getApp()->settings->lowercaseLink) { + // if (getSettings()->lowercaseLink) { // QRegularExpression httpRegex("\\bhttps?://", // QRegularExpression::CaseInsensitiveOption); QRegularExpression // ftpRegex("\\bftps?://", @@ -343,7 +331,7 @@ void TwitchMessageBuilder::parseRoomID() if (iterator != std::end(this->tags)) { this->roomID_ = iterator.value().toString(); - if (this->twitchChannel->getRoomId().isEmpty()) { + if (this->twitchChannel->roomId().isEmpty()) { this->twitchChannel->setRoomId(this->roomID_); } } @@ -447,36 +435,36 @@ void TwitchMessageBuilder::appendUsername() // IrcManager::getInstance().getUser().getUserName(); } else if (this->args.isReceivedWhisper) { // Sender username - this->emplace(usernameText, MessageElementFlag::Text, + this->emplace(usernameText, MessageElementFlag::Username, this->usernameColor_, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, this->userName}); + ->setLink({Link::UserWhisper, this->message().displayName}); auto currentUser = app->accounts->twitch.getCurrent(); // Separator - this->emplace("->", MessageElementFlag::Text, + this->emplace("->", MessageElementFlag::Username, app->themes->messages.textColors.system, FontStyle::ChatMedium); - QColor selfColor = currentUser->color; + QColor selfColor = currentUser->color(); if (!selfColor.isValid()) { selfColor = app->themes->messages.textColors.system; } // Your own username this->emplace(currentUser->getUserName() + ":", - MessageElementFlag::Text, selfColor, + MessageElementFlag::Username, selfColor, FontStyle::ChatMediumBold); } else { if (!this->action_) { usernameText += ":"; } - this->emplace(usernameText, MessageElementFlag::Text, + this->emplace(usernameText, MessageElementFlag::Username, this->usernameColor_, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, this->userName}); + ->setLink({Link::UserInfo, this->message().displayName}); } } @@ -492,16 +480,16 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) QString currentUsername = currentUser->getUserName(); if (this->ircMessage->nick() == currentUsername) { - currentUser->color = this->usernameColor_; + currentUser->setColor(this->usernameColor_); // Do nothing. Highlights cannot be triggered by yourself return; } // update the media player url if necessary QUrl highlightSoundUrl; - if (app->settings->customHighlightSound) { + if (getSettings()->customHighlightSound) { highlightSoundUrl = - QUrl::fromLocalFile(app->settings->pathHighlightSound.getValue()); + QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue()); } else { highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav"); } @@ -519,10 +507,10 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) std::vector userHighlights = app->highlights->highlightedUsers.getVector(); - if (app->settings->enableHighlightsSelf && currentUsername.size() > 0) { + if (getSettings()->enableHighlightsSelf && currentUsername.size() > 0) { HighlightPhrase selfHighlight( - currentUsername, app->settings->enableHighlightTaskbar, - app->settings->enableHighlightSound, false); + currentUsername, getSettings()->enableHighlightTaskbar, + getSettings()->enableHighlightSound, false); activeHighlights.emplace_back(std::move(selfHighlight)); } @@ -535,7 +523,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) if (!app->highlights->blacklistContains(this->ircMessage->nick())) { for (const HighlightPhrase &highlight : activeHighlights) { if (highlight.isMatch(this->originalMessage_)) { - Log("Highlight because {} matches {}", this->originalMessage_, + log("Highlight because {} matches {}", this->originalMessage_, highlight.getPattern()); doHighlight = true; @@ -557,7 +545,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) } for (const HighlightPhrase &userHighlight : userHighlights) { if (userHighlight.isMatch(this->ircMessage->nick())) { - Log("Highlight because user {} sent a message", + log("Highlight because user {} sent a message", this->ircMessage->nick()); doHighlight = true; @@ -581,7 +569,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) if (!isPastMsg) { if (playSound && - (!hasFocus || app->settings->highlightAlwaysPlaySound)) { + (!hasFocus || getSettings()->highlightAlwaysPlaySound)) { player->play(); } @@ -637,18 +625,22 @@ void TwitchMessageBuilder::appendTwitchEmote( Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) { + // Special channels, like /whispers and /channels return here + // This means they will not render any BTTV or FFZ emotes + if (this->twitchChannel == nullptr) { + return Failure; + } + auto flags = MessageElementFlags(); auto emote = boost::optional{}; - if ((emote = getApp()->emotes->bttv.getGlobalEmote(name))) { + if ((emote = this->twitchChannel->globalBttv().emote(name))) { flags = MessageElementFlag::BttvEmote; - } else if (twitchChannel && - (emote = this->twitchChannel->getBttvEmote(name))) { + } else if ((emote = this->twitchChannel->bttvEmote(name))) { flags = MessageElementFlag::BttvEmote; - } else if ((emote = getApp()->emotes->ffz.getGlobalEmote(name))) { + } else if ((emote = this->twitchChannel->globalFfz().emote(name))) { flags = MessageElementFlag::FfzEmote; - } else if (twitchChannel && - (emote = this->twitchChannel->getFfzEmote(name))) { + } else if ((emote = this->twitchChannel->ffzEmote(name))) { flags = MessageElementFlag::FfzEmote; } @@ -661,39 +653,22 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) } // fourtf: this is ugly -// maybe put the individual badges into a map instead of this -// mess void TwitchMessageBuilder::appendTwitchBadges() { auto app = getApp(); auto iterator = this->tags.find("badges"); + if (iterator == this->tags.end()) return; - if (iterator == this->tags.end()) { - // No badges in this message - return; - } - - QStringList badges = iterator.value().toString().split(','); - - for (QString badge : badges) { - if (badge.isEmpty()) { - continue; - } - + for (QString badge : iterator.value().toString().split(',')) { if (badge.startsWith("bits/")) { - // if (!app->resources->dynamicBadgesLoaded) { - // // Do nothing - // continue; - // } - QString cheerAmount = badge.mid(5); QString tooltip = QString("Twitch cheer ") + cheerAmount; // Try to fetch channel-specific bit badge try { if (twitchChannel) - if (const auto &badge = this->twitchChannel->getTwitchBadge( + if (const auto &badge = this->twitchChannel->twitchBadge( "bits", cheerAmount)) { this->emplace( badge.get(), MessageElementFlag::BadgeVanity) @@ -705,53 +680,46 @@ void TwitchMessageBuilder::appendTwitchBadges() } // Use default bit badge - // try { - // const auto &badge = - // app->resources->badgeSets.at("bits").versions.at(cheerAmount); - // this->emplace(badge.badgeImage1x, - // MessageElementFlag::BadgeVanity) - // ->setTooltip(tooltip); - //} catch (const std::out_of_range &) { - // Log("No default bit badge for version {} found", cheerAmount); - // continue; - //} + if (auto badge = this->twitchChannel->globalTwitchBadges().badge( + "bits", cheerAmount)) { + this->emplace(badge.get(), + MessageElementFlag::BadgeVanity) + ->setTooltip(tooltip); + } } else if (badge == "staff/1") { this->emplace( - Image::fromNonOwningPixmap(&app->resources->twitch.staff), + Image::fromPixmap(app->resources->twitch.staff), MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Staff"); } else if (badge == "admin/1") { this->emplace( - Image::fromNonOwningPixmap(&app->resources->twitch.admin), + Image::fromPixmap(app->resources->twitch.admin), MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Admin"); } else if (badge == "global_mod/1") { this->emplace( - Image::fromNonOwningPixmap( - &app->resources->twitch.globalmod), + Image::fromPixmap(app->resources->twitch.globalmod), MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Global Moderator"); } else if (badge == "moderator/1") { // TODO: Implement custom FFZ moderator badge this->emplace( - Image::fromNonOwningPixmap( - &app->resources->twitch.moderator), + Image::fromPixmap(app->resources->twitch.moderator), MessageElementFlag::BadgeChannelAuthority) ->setTooltip("Twitch Channel Moderator"); } else if (badge == "turbo/1") { this->emplace( - Image::fromNonOwningPixmap(&app->resources->twitch.turbo), + Image::fromPixmap(app->resources->twitch.turbo), MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Turbo Subscriber"); } else if (badge == "broadcaster/1") { this->emplace( - Image::fromNonOwningPixmap( - &app->resources->twitch.broadcaster), + Image::fromPixmap(app->resources->twitch.broadcaster), MessageElementFlag::BadgeChannelAuthority) ->setTooltip("Twitch Broadcaster"); } else if (badge == "premium/1") { this->emplace( - Image::fromNonOwningPixmap(&app->resources->twitch.prime), + Image::fromPixmap(app->resources->twitch.prime), MessageElementFlag::BadgeVanity) ->setTooltip("Twitch Prime Subscriber"); } else if (badge.startsWith("partner/")) { @@ -759,8 +727,8 @@ void TwitchMessageBuilder::appendTwitchBadges() switch (index) { case 1: { this->emplace( - Image::fromNonOwningPixmap( - &app->resources->twitch.verified, 0.25), + Image::fromPixmap(app->resources->twitch.verified, + 0.25), MessageElementFlag::BadgeVanity) ->setTooltip("Twitch Verified"); } break; @@ -771,80 +739,30 @@ void TwitchMessageBuilder::appendTwitchBadges() } break; } } else if (badge.startsWith("subscriber/")) { - // if (channelResources.loaded == false) { - // // qDebug() << "Channel resources are not loaded, - // can't add the subscriber - // // badge"; - // continue; - // } + if (auto badgeEmote = this->twitchChannel->twitchBadge( + "subscriber", badge.mid(11))) { + this->emplace( + badgeEmote.get(), MessageElementFlag::BadgeSubscription) + ->setTooltip((*badgeEmote)->tooltip.string); + continue; + } - // auto badgeSetIt = channelResources.badgeSets.find("subscriber"); - // if (badgeSetIt == channelResources.badgeSets.end()) { - // // Fall back to default badge - // this->emplace(app->resources->badgeSubscriber, - // MessageElementFlag::BadgeSubscription) - // ->setTooltip("Twitch Subscriber"); - // continue; - //} - - // const auto &badgeSet = badgeSetIt->second; - - // std::string versionKey = badge.mid(11).toStdString(); - - // auto badgeVersionIt = badgeSet.versions.find(versionKey); - - // if (badgeVersionIt == badgeSet.versions.end()) { - // // Fall back to default badge - // this->emplace(app->resources->badgeSubscriber, - // MessageElementFlag::BadgeSubscription) - // ->setTooltip("Twitch Subscriber"); - // continue; - //} - - // auto &badgeVersion = badgeVersionIt->second; - - // this->emplace(badgeVersion.badgeImage1x, - // MessageElementFlag::BadgeSubscription) - // ->setTooltip("Twitch " + - // QString::fromStdString(badgeVersion.title)); + // use default subscriber badge if custom one not found + this->emplace( + Image::fromPixmap(app->resources->twitch.subscriber, 0.25), + MessageElementFlag::BadgeSubscription) + ->setTooltip("Twitch Subscriber"); } else { - // if (!app->resources->dynamicBadgesLoaded) { - // // Do nothing - // continue; - //} + auto splits = badge.split('/'); + if (splits.size() != 2) continue; - // QStringList parts = badge.split('/'); - - // if (parts.length() != 2) { - // qDebug() << "Bad number of parts: " << parts.length() << " in - // " << parts; continue; - //} - - // MessageElementFlags badgeType = - // MessageElementFlag::BadgeVanity; - - // std::string badgeSetKey = parts[0].toStdString(); - // std::string versionKey = parts[1].toStdString(); - - // try { - // auto &badgeSet = app->resources->badgeSets.at(badgeSetKey); - - // try { - // auto &badgeVersion = badgeSet.versions.at(versionKey); - - // this->emplace(badgeVersion.badgeImage1x, - // badgeType) - // ->setTooltip("Twitch " + - // QString::fromStdString(badgeVersion.title)); - // } catch (const std::exception &e) { - // qDebug() << "Exception caught:" << e.what() - // << "when trying to fetch badge version " << - // versionKey.c_str(); - // } - //} catch (const std::exception &e) { - // qDebug() << "No badge set with key" << badgeSetKey.c_str() - // << ". Exception: " << e.what(); - //} + if (auto badgeEmote = + this->twitchChannel->twitchBadge(splits[0], splits[1])) { + this->emplace(badgeEmote.get(), + MessageElementFlag::BadgeVanity) + ->setTooltip((*badgeEmote)->tooltip.string); + continue; + } } } } diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index c82364a67..30b476f97 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -1,16 +1,18 @@ #pragma once +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" #include "messages/MessageBuilder.hpp" -#include "messages/MessageParseArgs.hpp" -#include "singletons/Emotes.hpp" #include - #include #include namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + class Channel; class TwitchChannel; diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp index 47d6427f8..65eca1fa3 100644 --- a/src/providers/twitch/TwitchParseCheerEmotes.cpp +++ b/src/providers/twitch/TwitchParseCheerEmotes.cpp @@ -8,235 +8,235 @@ namespace chatterino { namespace { -template -inline bool ReadValue(const rapidjson::Value &object, const char *key, - Type &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.Is()) { - return false; - } - - out = value.Get(); - - return true; -} - -template <> -inline bool ReadValue(const rapidjson::Value &object, const char *key, - QString &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsString()) { - return false; - } - - out = value.GetString(); - - return true; -} - -template <> -inline bool ReadValue>(const rapidjson::Value &object, - const char *key, - std::vector &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsArray()) { - return false; - } - - for (const rapidjson::Value &innerValue : value.GetArray()) { - if (!innerValue.IsString()) { + template + inline bool ReadValue(const rapidjson::Value &object, const char *key, + Type &out) + { + if (!object.HasMember(key)) { return false; } - out.emplace_back(innerValue.GetString()); - } + const auto &value = object[key]; - return true; -} - -// Parse a single cheermote set (or "action") from the twitch api -inline bool ParseSingleCheermoteSet(JSONCheermoteSet &set, - const rapidjson::Value &action) -{ - if (!action.IsObject()) { - return false; - } - - if (!ReadValue(action, "prefix", set.prefix)) { - return false; - } - - if (!ReadValue(action, "scales", set.scales)) { - return false; - } - - if (!ReadValue(action, "backgrounds", set.backgrounds)) { - return false; - } - - if (!ReadValue(action, "states", set.states)) { - return false; - } - - if (!ReadValue(action, "type", set.type)) { - return false; - } - - if (!ReadValue(action, "updated_at", set.updatedAt)) { - return false; - } - - if (!ReadValue(action, "priority", set.priority)) { - return false; - } - - // Tiers - if (!action.HasMember("tiers")) { - return false; - } - - const auto &tiersValue = action["tiers"]; - - if (!tiersValue.IsArray()) { - return false; - } - - for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { - JSONCheermoteSet::CheermoteTier tier; - - if (!tierValue.IsObject()) { + if (!value.Is()) { return false; } - if (!ReadValue(tierValue, "min_bits", tier.minBits)) { + out = value.Get(); + + return true; + } + + template <> + inline bool ReadValue(const rapidjson::Value &object, + const char *key, QString &out) + { + if (!object.HasMember(key)) { return false; } - if (!ReadValue(tierValue, "id", tier.id)) { + const auto &value = object[key]; + + if (!value.IsString()) { return false; } - if (!ReadValue(tierValue, "color", tier.color)) { + out = value.GetString(); + + return true; + } + + template <> + inline bool ReadValue>(const rapidjson::Value &object, + const char *key, + std::vector &out) + { + if (!object.HasMember(key)) { return false; } - // Images - if (!tierValue.HasMember("images")) { + const auto &value = object[key]; + + if (!value.IsArray()) { return false; } - const auto &imagesValue = tierValue["images"]; - - if (!imagesValue.IsObject()) { - return false; - } - - // Read images object - for (const auto &imageBackgroundValue : imagesValue.GetObject()) { - QString background = imageBackgroundValue.name.GetString(); - bool backgroundExists = false; - for (const auto &bg : set.backgrounds) { - if (background == bg) { - backgroundExists = true; - break; - } + for (const rapidjson::Value &innerValue : value.GetArray()) { + if (!innerValue.IsString()) { + return false; } - if (!backgroundExists) { - continue; + out.emplace_back(innerValue.GetString()); + } + + return true; + } + + // Parse a single cheermote set (or "action") from the twitch api + inline bool ParseSingleCheermoteSet(JSONCheermoteSet &set, + const rapidjson::Value &action) + { + if (!action.IsObject()) { + return false; + } + + if (!ReadValue(action, "prefix", set.prefix)) { + return false; + } + + if (!ReadValue(action, "scales", set.scales)) { + return false; + } + + if (!ReadValue(action, "backgrounds", set.backgrounds)) { + return false; + } + + if (!ReadValue(action, "states", set.states)) { + return false; + } + + if (!ReadValue(action, "type", set.type)) { + return false; + } + + if (!ReadValue(action, "updated_at", set.updatedAt)) { + return false; + } + + if (!ReadValue(action, "priority", set.priority)) { + return false; + } + + // Tiers + if (!action.HasMember("tiers")) { + return false; + } + + const auto &tiersValue = action["tiers"]; + + if (!tiersValue.IsArray()) { + return false; + } + + for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { + JSONCheermoteSet::CheermoteTier tier; + + if (!tierValue.IsObject()) { + return false; } - const rapidjson::Value &imageBackgroundStates = - imageBackgroundValue.value; - if (!imageBackgroundStates.IsObject()) { - continue; + if (!ReadValue(tierValue, "min_bits", tier.minBits)) { + return false; } - // Read each key which represents a background - for (const auto &imageBackgroundState : - imageBackgroundStates.GetObject()) { - QString state = imageBackgroundState.name.GetString(); - bool stateExists = false; - for (const auto &_state : set.states) { - if (state == _state) { - stateExists = true; + if (!ReadValue(tierValue, "id", tier.id)) { + return false; + } + + if (!ReadValue(tierValue, "color", tier.color)) { + return false; + } + + // Images + if (!tierValue.HasMember("images")) { + return false; + } + + const auto &imagesValue = tierValue["images"]; + + if (!imagesValue.IsObject()) { + return false; + } + + // Read images object + for (const auto &imageBackgroundValue : imagesValue.GetObject()) { + QString background = imageBackgroundValue.name.GetString(); + bool backgroundExists = false; + for (const auto &bg : set.backgrounds) { + if (background == bg) { + backgroundExists = true; break; } } - if (!stateExists) { + if (!backgroundExists) { continue; } - const rapidjson::Value &imageScalesValue = - imageBackgroundState.value; - if (!imageScalesValue.IsObject()) { + const rapidjson::Value &imageBackgroundStates = + imageBackgroundValue.value; + if (!imageBackgroundStates.IsObject()) { continue; } - // Read each key which represents a scale - for (const auto &imageScaleValue : - imageScalesValue.GetObject()) { - QString scale = imageScaleValue.name.GetString(); - bool scaleExists = false; - for (const auto &_scale : set.scales) { - if (scale == _scale) { - scaleExists = true; + // Read each key which represents a background + for (const auto &imageBackgroundState : + imageBackgroundStates.GetObject()) { + QString state = imageBackgroundState.name.GetString(); + bool stateExists = false; + for (const auto &_state : set.states) { + if (state == _state) { + stateExists = true; break; } } - if (!scaleExists) { + if (!stateExists) { continue; } - const rapidjson::Value &imageScaleURLValue = - imageScaleValue.value; - if (!imageScaleURLValue.IsString()) { + const rapidjson::Value &imageScalesValue = + imageBackgroundState.value; + if (!imageScalesValue.IsObject()) { continue; } - QString url = imageScaleURLValue.GetString(); + // Read each key which represents a scale + for (const auto &imageScaleValue : + imageScalesValue.GetObject()) { + QString scale = imageScaleValue.name.GetString(); + bool scaleExists = false; + for (const auto &_scale : set.scales) { + if (scale == _scale) { + scaleExists = true; + break; + } + } - bool ok = false; - qreal scaleNumber = scale.toFloat(&ok); - if (!ok) { - continue; + if (!scaleExists) { + continue; + } + + const rapidjson::Value &imageScaleURLValue = + imageScaleValue.value; + if (!imageScaleURLValue.IsString()) { + continue; + } + + QString url = imageScaleURLValue.GetString(); + + bool ok = false; + qreal scaleNumber = scale.toFloat(&ok); + if (!ok) { + continue; + } + + qreal chatterinoScale = 1 / scaleNumber; + + auto image = Image::fromUrl({url}, chatterinoScale); + + // TODO(pajlada): Fill in name and tooltip + tier.images[background][state][scale] = image; } - - qreal chatterinoScale = 1 / scaleNumber; - - auto image = Image::fromUrl({url}, chatterinoScale); - - // TODO(pajlada): Fill in name and tooltip - tier.images[background][state][scale] = image; } } + + set.tiers.emplace_back(tier); } - set.tiers.emplace_back(tier); + return true; } - - return true; -} } // namespace // Look through the results of diff --git a/src/providers/twitch/TwitchServer.cpp b/src/providers/twitch/TwitchServer.cpp index 8a5e48cab..d1e03b63b 100644 --- a/src/providers/twitch/TwitchServer.cpp +++ b/src/providers/twitch/TwitchServer.cpp @@ -7,12 +7,12 @@ #include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchHelpers.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "util/PostToThread.hpp" #include - #include // using namespace Communi; @@ -39,6 +39,10 @@ void TwitchServer::initialize(Settings &settings, Paths &paths) { getApp()->accounts->twitch.currentUserChanged.connect( [this]() { postToThread([this] { this->connect(); }); }); + + this->twitchBadges.loadTwitchBadges(); + this->bttv.loadEmotes(); + this->ffz.loadEmotes(); } void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, @@ -79,10 +83,12 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, std::shared_ptr TwitchServer::createChannel(const QString &channelName) { - TwitchChannel *channel = new TwitchChannel(channelName); + auto channel = std::shared_ptr(new TwitchChannel( + channelName, this->twitchBadges, this->bttv, this->ffz)); + channel->initialize(); channel->sendMessageSignal.connect( - [this, channel](auto &chan, auto &msg, bool &sent) { + [this, channel = channel.get()](auto &chan, auto &msg, bool &sent) { this->onMessageSendRequested(channel, msg, sent); }); @@ -175,7 +181,7 @@ std::shared_ptr TwitchServer::getChannelOrEmptyByID( auto twitchChannel = std::dynamic_pointer_cast(channel); if (!twitchChannel) continue; - if (twitchChannel->getRoomId() == channelId) { + if (twitchChannel->roomId() == channelId) { return twitchChannel; } } diff --git a/src/providers/twitch/TwitchServer.hpp b/src/providers/twitch/TwitchServer.hpp index edc06ef6a..60026a142 100644 --- a/src/providers/twitch/TwitchServer.hpp +++ b/src/providers/twitch/TwitchServer.hpp @@ -1,10 +1,13 @@ #pragma once -#include "common/MutexValue.hpp" +#include "common/Atomic.hpp" +#include "common/Channel.hpp" #include "common/Singleton.hpp" +#include "pajlada/signals/signalholder.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" #include "providers/irc/AbstractIrcServer.hpp" -#include "providers/twitch/TwitchAccount.hpp" -#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchBadges.hpp" #include #include @@ -14,8 +17,8 @@ namespace chatterino { class Settings; class Paths; - class PubSub; +class TwitchChannel; class TwitchServer final : public AbstractIrcServer, public Singleton { @@ -29,7 +32,7 @@ public: std::shared_ptr getChannelOrEmptyByID(const QString &channelID); - MutexValue lastUserThatWhisperedMe; + Atomic lastUserThatWhisperedMe; const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; @@ -66,6 +69,9 @@ private: std::chrono::steady_clock::time_point lastErrorTimeAmount_; bool singleConnection_ = false; + TwitchBadges twitchBadges; + BttvEmotes bttv; + FfzEmotes ffz; pajlada::Signals::SignalHolder signalHolder_; }; diff --git a/src/providers/twitch/TwitchUser.hpp b/src/providers/twitch/TwitchUser.hpp index 74a9c1252..27539386d 100644 --- a/src/providers/twitch/TwitchUser.hpp +++ b/src/providers/twitch/TwitchUser.hpp @@ -34,43 +34,43 @@ struct TwitchUser { namespace pajlada { namespace Settings { -template <> -struct Deserialize { - static chatterino::TwitchUser get(const rapidjson::Value &value, - bool *error = nullptr) - { - using namespace chatterino; + template <> + struct Deserialize { + static chatterino::TwitchUser get(const rapidjson::Value &value, + bool *error = nullptr) + { + using namespace chatterino; - TwitchUser user; + TwitchUser user; + + if (!value.IsObject()) { + PAJLADA_REPORT_ERROR(error) + PAJLADA_THROW_EXCEPTION( + "Deserialized rapidjson::Value is wrong type"); + return user; + } + + if (!rj::getSafe(value, "_id", user.id)) { + PAJLADA_REPORT_ERROR(error) + PAJLADA_THROW_EXCEPTION("Missing ID key"); + return user; + } + + if (!rj::getSafe(value, "name", user.name)) { + PAJLADA_REPORT_ERROR(error) + PAJLADA_THROW_EXCEPTION("Missing name key"); + return user; + } + + if (!rj::getSafe(value, "display_name", user.displayName)) { + PAJLADA_REPORT_ERROR(error) + PAJLADA_THROW_EXCEPTION("Missing display name key"); + return user; + } - if (!value.IsObject()) { - PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION( - "Deserialized rapidjson::Value is wrong type"); return user; } - - if (!rj::getSafe(value, "_id", user.id)) { - PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION("Missing ID key"); - return user; - } - - if (!rj::getSafe(value, "name", user.name)) { - PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION("Missing name key"); - return user; - } - - if (!rj::getSafe(value, "display_name", user.displayName)) { - PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION("Missing display name key"); - return user; - } - - return user; - } -}; + }; } // namespace Settings } // namespace pajlada diff --git a/src/singletons/Emotes.cpp b/src/singletons/Emotes.cpp index 0be870a5e..9916d2046 100644 --- a/src/singletons/Emotes.cpp +++ b/src/singletons/Emotes.cpp @@ -15,8 +15,6 @@ void Emotes::initialize(Settings &settings, Paths &paths) [] { getApp()->accounts->twitch.getCurrent()->loadEmotes(); }); this->emojis.load(); - this->bttv.loadGlobalEmotes(); - this->ffz.loadGlobalEmotes(); this->gifTimer.initialize(); } diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index e991fe674..fc475d752 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -25,8 +25,6 @@ public: bool isIgnoredEmote(const QString &emote); TwitchEmotes twitch; - BttvEmotes bttv; - FfzEmotes ffz; Emojis emojis; GIFTimer gifTimer; diff --git a/src/singletons/Fonts.cpp b/src/singletons/Fonts.cpp index 69f327285..991646acd 100644 --- a/src/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -2,22 +2,23 @@ #include "Application.hpp" #include "debug/AssertInGuiThread.hpp" +#include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include #include #ifdef Q_OS_WIN32 -#define DEFAULT_FONT_FAMILY "Segoe UI" -#define DEFAULT_FONT_SIZE 10 +# define DEFAULT_FONT_FAMILY "Segoe UI" +# define DEFAULT_FONT_SIZE 10 #else -#ifdef Q_OS_MACOS -#define DEFAULT_FONT_FAMILY "Helvetica Neue" -#define DEFAULT_FONT_SIZE 12 -#else -#define DEFAULT_FONT_FAMILY "Arial" -#define DEFAULT_FONT_SIZE 11 -#endif +# ifdef Q_OS_MACOS +# define DEFAULT_FONT_FAMILY "Helvetica Neue" +# define DEFAULT_FONT_SIZE 12 +# else +# define DEFAULT_FONT_FAMILY "Arial" +# define DEFAULT_FONT_SIZE 11 +# endif #endif namespace chatterino { @@ -26,7 +27,7 @@ Fonts::Fonts() : chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) , chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) { - this->fontsByType_.resize(size_t(EndType)); + this->fontsByType_.resize(size_t(FontStyle::EndType)); } void Fonts::initialize(Settings &, Paths &) @@ -61,21 +62,21 @@ void Fonts::initialize(Settings &, Paths &) }); } -QFont Fonts::getFont(Fonts::Type type, float scale) +QFont Fonts::getFont(FontStyle type, float scale) { return this->getOrCreateFontData(type, scale).font; } -QFontMetrics Fonts::getFontMetrics(Fonts::Type type, float scale) +QFontMetrics Fonts::getFontMetrics(FontStyle type, float scale) { return this->getOrCreateFontData(type, scale).metrics; } -Fonts::FontData &Fonts::getOrCreateFontData(Type type, float scale) +Fonts::FontData &Fonts::getOrCreateFontData(FontStyle type, float scale) { assertInGuiThread(); - assert(type >= 0 && type < EndType); + assert(type < FontStyle::EndType); auto &map = this->fontsByType_[size_t(type)]; @@ -94,23 +95,22 @@ Fonts::FontData &Fonts::getOrCreateFontData(Type type, float scale) return result.first->second; } -Fonts::FontData Fonts::createFontData(Type type, float scale) +Fonts::FontData Fonts::createFontData(FontStyle type, float scale) { // check if it's a chat (scale the setting) - if (type >= ChatStart && type <= ChatEnd) { - static std::unordered_map sizeScale{ - {ChatSmall, {0.6f, false, QFont::Normal}}, - {ChatMediumSmall, {0.8f, false, QFont::Normal}}, - {ChatMedium, {1, false, QFont::Normal}}, - {ChatMediumBold, - {1, false, - QFont::Weight(getApp()->settings->boldScale.getValue())}}, - {ChatMediumItalic, {1, true, QFont::Normal}}, - {ChatLarge, {1.2f, false, QFont::Normal}}, - {ChatVeryLarge, {1.4f, false, QFont::Normal}}, + if (type >= FontStyle::ChatStart && type <= FontStyle::ChatEnd) { + static std::unordered_map sizeScale{ + {FontStyle::ChatSmall, {0.6f, false, QFont::Normal}}, + {FontStyle::ChatMediumSmall, {0.8f, false, QFont::Normal}}, + {FontStyle::ChatMedium, {1, false, QFont::Normal}}, + {FontStyle::ChatMediumBold, + {1, false, QFont::Weight(getSettings()->boldScale.getValue())}}, + {FontStyle::ChatMediumItalic, {1, true, QFont::Normal}}, + {FontStyle::ChatLarge, {1.2f, false, QFont::Normal}}, + {FontStyle::ChatVeryLarge, {1.4f, false, QFont::Normal}}, }; - sizeScale[ChatMediumBold] = { - 1, false, QFont::Weight(getApp()->settings->boldScale.getValue())}; + sizeScale[FontStyle::ChatMediumBold] = { + 1, false, QFont::Weight(getSettings()->boldScale.getValue())}; auto data = sizeScale[type]; return FontData( QFont(QString::fromStdString(this->chatFontFamily.getValue()), @@ -126,11 +126,11 @@ Fonts::FontData Fonts::createFontData(Type type, float scale) constexpr float multiplier = 1.f; #endif - static std::unordered_map defaultSize{ - {Tiny, {8, "Monospace", false, QFont::Normal}}, - {UiMedium, + static std::unordered_map defaultSize{ + {FontStyle::Tiny, {8, "Monospace", false, QFont::Normal}}, + {FontStyle::UiMedium, {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, - {UiTabs, + {FontStyle::UiTabs, {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, }; diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index 59c6550c3..61ba393d6 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -16,6 +16,27 @@ namespace chatterino { class Settings; class Paths; +enum class FontStyle : uint8_t { + Tiny, + ChatSmall, + ChatMediumSmall, + ChatMedium, + ChatMediumBold, + ChatMediumItalic, + ChatLarge, + ChatVeryLarge, + + UiMedium, + UiTabs, + + // don't remove this value + EndType, + + // make sure to update these values accordingly! + ChatStart = ChatSmall, + ChatEnd = ChatVeryLarge, +}; + class Fonts final : public Singleton { public: @@ -24,29 +45,9 @@ public: virtual void initialize(Settings &settings, Paths &paths) override; // font data gets set in createFontData(...) - enum Type : uint8_t { - Tiny, - ChatSmall, - ChatMediumSmall, - ChatMedium, - ChatMediumBold, - ChatMediumItalic, - ChatLarge, - ChatVeryLarge, - UiMedium, - UiTabs, - - // don't remove this value - EndType, - - // make sure to update these values accordingly! - ChatStart = ChatSmall, - ChatEnd = ChatVeryLarge, - }; - - QFont getFont(Type type, float scale); - QFontMetrics getFontMetrics(Type type, float scale); + QFont getFont(FontStyle type, float scale); + QFontMetrics getFontMetrics(FontStyle type, float scale); pajlada::Settings::Setting chatFontFamily; pajlada::Settings::Setting chatFontSize; @@ -78,12 +79,10 @@ private: QFont::Weight weight; }; - FontData &getOrCreateFontData(Type type, float scale); - FontData createFontData(Type type, float scale); + FontData &getOrCreateFontData(FontStyle type, float scale); + FontData createFontData(FontStyle type, float scale); std::vector> fontsByType_; }; -using FontStyle = Fonts::Type; - } // namespace chatterino diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 4afea4714..ed113f716 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -17,11 +17,11 @@ namespace ipc = boost::interprocess; #ifdef Q_OS_WIN -#include +# include -#include -#include "singletons/WindowManager.hpp" -#include "widgets/AttachedWindow.hpp" +# include +# include "singletons/WindowManager.hpp" +# include "widgets/AttachedWindow.hpp" #endif #include @@ -200,7 +200,7 @@ void NativeMessagingServer::ReceiverThread::handleMessage( if (_type == "twitch") { postToThread([=] { if (!name.isEmpty()) { - app->twitch.server->watchingChannel.update( + app->twitch.server->watchingChannel.reset( app->twitch.server->getOrAddChannel(name)); } diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index 21d1f57ad..90a094e76 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -1,5 +1,7 @@ #include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" + #include #include #include @@ -33,6 +35,27 @@ bool Paths::isPortable() return this->portable_.get(); } +QString Paths::cacheDirectory() +{ + static QStringSetting cachePathSetting = [] { + QStringSetting cachePathSetting("/cache/path"); + + cachePathSetting.connect([](const auto &newPath, auto) { + QDir().mkpath(newPath); // + }); + + return cachePathSetting; + }(); + + auto path = cachePathSetting.getValue(); + + if (path == "") { + return this->cacheDirectory_; + } + + return path; +} + void Paths::initAppFilePathHash() { this->applicationFilePathHash = @@ -104,7 +127,7 @@ void Paths::initSubDirectories() makePath(""); this->settingsDirectory = makePath("Settings"); - this->cacheDirectory = makePath("Cache"); + this->cacheDirectory_ = makePath("Cache"); this->messageLogDirectory = makePath("Logs"); this->miscDirectory = makePath("Misc"); } diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index 071feb737..de67e8065 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -19,9 +19,6 @@ public: // Directory for settings files. Same as /Settings QString settingsDirectory; - // Directory for cache files. Same as /Misc - QString cacheDirectory; - // Directory for message log files. Same as /Misc QString messageLogDirectory; @@ -34,6 +31,8 @@ public: bool createFolder(const QString &folderPath); bool isPortable(); + QString cacheDirectory(); + private: void initAppFilePathHash(); void initCheckPortable(); @@ -41,8 +40,11 @@ private: void initSubDirectories(); boost::optional portable_; + + // Directory for cache files. Same as /Misc + QString cacheDirectory_; }; -[[deprecated]] Paths *getPaths(); +Paths *getPaths(); } // namespace chatterino diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index b380a0119..ea5318ab3 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -50,7 +50,7 @@ void Settings::saveSnapshot() this->snapshot_.reset(d); - Log("hehe: {}", pajlada::Settings::SettingManager::stringify(*d)); + log("hehe: {}", pajlada::Settings::SettingManager::stringify(*d)); } void Settings::restoreSnapshot() @@ -64,14 +64,14 @@ void Settings::restoreSnapshot() for (const auto &weakSetting : _settings) { auto setting = weakSetting.lock(); if (!setting) { - Log("Error stage 1 of loading"); + log("Error stage 1 of loading"); continue; } const char *path = setting->getPath().c_str(); if (!snapshotObject.HasMember(path)) { - Log("Error stage 2 of loading"); + log("Error stage 2 of loading"); continue; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 9a286ab33..31e9881bc 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -5,7 +5,6 @@ #include "common/ChatterinoSetting.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" -#include "messages/MessageElement.hpp" #include #include @@ -26,6 +25,8 @@ public: /// Appearance BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true}; + BoolSetting enableAnimationsWhenFocused = { + "/appearance/enableAnimationsWhenFocused", false}; QStringSetting timestampFormat = {"/appearance/messages/timestampFormat", "h:mm"}; BoolSetting showBadges = {"/appearance/messages/showBadges", true}; @@ -98,12 +99,6 @@ public: BoolSetting enableGifAnimations = {"/emotes/enableGifAnimations", true}; FloatSetting emoteScale = {"/emotes/scale", 1.f}; - // 0 = No preference - // 1 = 1x - // 2 = 2x - // 3 = 3x - IntSetting preferredEmoteQuality = {"/emotes/preferredEmoteQuality", 0}; - QStringSetting emojiSet = {"/emotes/emojiSet", "EmojiOne 2"}; /// Links @@ -164,6 +159,8 @@ public: IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; + QStringSetting cachePath = {"/cache/path", ""}; + void saveSnapshot(); void restoreSnapshot(); @@ -173,6 +170,6 @@ private: std::unique_ptr snapshot_; }; -[[deprecated]] Settings *getSettings(); +Settings *getSettings(); } // namespace chatterino diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index eb20a7647..470fd7543 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -10,21 +10,21 @@ namespace chatterino { namespace detail { -double getMultiplierByTheme(const QString &themeName) -{ - if (themeName == "Light") { - return 0.8; - } else if (themeName == "White") { - return 1.0; - } else if (themeName == "Black") { - return -1.0; - } else if (themeName == "Dark") { + double getMultiplierByTheme(const QString &themeName) + { + if (themeName == "Light") { + return 0.8; + } else if (themeName == "White") { + return 1.0; + } else if (themeName == "Black") { + return -1.0; + } else if (themeName == "Dark") { + return -0.8; + } + return -0.8; } - return -0.8; -} - } // namespace detail Theme::Theme() diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 66b859aaa..6c88e7428 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -1,7 +1,7 @@ #pragma once -#include "common/SerializeCustom.hpp" #include "common/Singleton.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include #include diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index d56a8f510..5782b4bea 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -1,6 +1,7 @@ #include "Updates.hpp" #include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" #include "common/Version.hpp" #include "singletons/Paths.hpp" #include "util/CombinePath.hpp" diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 23b8c4e1b..02b711f7b 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -3,13 +3,20 @@ #include "Application.hpp" #include "debug/AssertInGuiThread.hpp" #include "debug/Log.hpp" +#include "messages/MessageElement.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Fonts.hpp" #include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "util/Clamp.hpp" #include "widgets/AccountSwitchPopupWidget.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/Window.hpp" #include "widgets/dialogs/SettingsDialog.hpp" +#include "widgets/helper/NotebookTab.hpp" +#include "widgets/splits/Split.hpp" +#include "widgets/splits/SplitContainer.hpp" #include #include @@ -174,7 +181,7 @@ Window &WindowManager::getSelectedWindow() return *this->selectedWindow_; } -Window &WindowManager::createWindow(Window::Type type) +Window &WindowManager::createWindow(WindowType type) { assertInGuiThread(); @@ -182,7 +189,7 @@ Window &WindowManager::createWindow(Window::Type type) this->windows_.push_back(window); window->show(); - if (type != Window::Type::Main) { + if (type != WindowType::Main) { window->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(window, &QWidget::destroyed, [this, window] { @@ -211,7 +218,7 @@ Window *WindowManager::windowAt(int index) if (index < 0 || (size_t)index >= this->windows_.size()) { return nullptr; } - Log("getting window at bad index {}", index); + log("getting window at bad index {}", index); return this->windows_.at(index); } @@ -239,16 +246,16 @@ void WindowManager::initialize(Settings &settings, Paths &paths) // get type QString type_val = window_obj.value("type").toString(); - Window::Type type = - type_val == "main" ? Window::Type::Main : Window::Type::Popup; + WindowType type = + type_val == "main" ? WindowType::Main : WindowType::Popup; - if (type == Window::Type::Main && mainWindow_ != nullptr) { - type = Window::Type::Popup; + if (type == WindowType::Main && mainWindow_ != nullptr) { + type = WindowType::Popup; } Window &window = createWindow(type); - if (type == Window::Type::Main) { + if (type == WindowType::Main) { mainWindow_ = &window; } @@ -308,7 +315,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths) } if (mainWindow_ == nullptr) { - mainWindow_ = &createWindow(Window::Type::Main); + mainWindow_ = &createWindow(WindowType::Main); mainWindow_->getNotebook().addPage(true); } @@ -344,15 +351,15 @@ void WindowManager::save() // window type switch (window->getType()) { - case Window::Type::Main: + case WindowType::Main: window_obj.insert("type", "main"); break; - case Window::Type::Popup: + case WindowType::Popup: window_obj.insert("type", "popup"); break; - case Window::Type::Attached:; + case WindowType::Attached:; } // window geometry @@ -399,7 +406,7 @@ void WindowManager::save() document.setObject(obj); // save file - QString settingsPath = app->paths->settingsDirectory + SETTINGS_FILENAME; + QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; QFile file(settingsPath); file.open(QIODevice::WriteOnly | QIODevice::Truncate); @@ -511,7 +518,7 @@ int WindowManager::clampUiScale(int scale) float WindowManager::getUiScaleValue() { - return getUiScaleValue(getApp()->settings->uiScale.getValue()); + return getUiScaleValue(getSettings()->uiScale.getValue()); } float WindowManager::getUiScaleValue(int scale) diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index eef2be2f1..74149876e 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -1,13 +1,20 @@ #pragma once +#include "common/Channel.hpp" +#include "common/FlagsEnum.hpp" #include "common/Singleton.hpp" -#include "widgets/Window.hpp" #include "widgets/splits/SplitContainer.hpp" namespace chatterino { class Settings; class Paths; +class Window; +class SplitContainer; + +enum class MessageElementFlag; +using MessageElementFlags = FlagsEnum; +enum class WindowType; class WindowManager final : public Singleton { @@ -34,7 +41,7 @@ public: Window &getMainWindow(); Window &getSelectedWindow(); - Window &createWindow(Window::Type type); + Window &createWindow(WindowType type); int windowCount(); Window *windowAt(int index); @@ -63,10 +70,10 @@ private: std::vector windows_; - Window *mainWindow_ = nullptr; - Window *selectedWindow_ = nullptr; + Window *mainWindow_{}; + Window *selectedWindow_{}; - MessageElementFlags wordFlags_ = MessageElementFlag::Default; + MessageElementFlags wordFlags_{}; pajlada::Settings::SettingListener wordFlagsListener_; }; diff --git a/src/singletons/helper/GifTimer.cpp b/src/singletons/helper/GifTimer.cpp index 49fbd98bf..8f3624523 100644 --- a/src/singletons/helper/GifTimer.cpp +++ b/src/singletons/helper/GifTimer.cpp @@ -10,19 +10,20 @@ void GIFTimer::initialize() { this->timer.setInterval(30); - getApp()->settings->enableGifAnimations.connect([this](bool enabled, auto) { - if (enabled) { + getSettings()->enableGifAnimations.connect([this](bool enabled, auto) { + if (enabled) this->timer.start(); - } else { + else this->timer.stop(); - } }); QObject::connect(&this->timer, &QTimer::timeout, [this] { + if (getSettings()->enableAnimationsWhenFocused && + qApp->activeWindow() == nullptr) + return; + this->signal.invoke(); - // fourtf: - auto app = getApp(); - app->windows->repaintGifEmotes(); + getApp()->windows->repaintGifEmotes(); }); } diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index 97a0b8478..c9081d2a6 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -30,11 +30,11 @@ LoggingChannel::LoggingChannel(const QString &_channelName) auto app = getApp(); - app->settings->logPath.connect([this](const QString &logPath, auto) { + getSettings()->logPath.connect([this](const QString &logPath, auto) { auto app = getApp(); if (logPath.isEmpty()) { - this->baseDirectory = app->paths->messageLogDirectory; + this->baseDirectory = getPaths()->messageLogDirectory; } else { this->baseDirectory = logPath; } @@ -65,13 +65,13 @@ void LoggingChannel::openLogFile() this->baseDirectory + QDir::separator() + this->subDirectory; if (!QDir().mkpath(directory)) { - Log("Unable to create logging path"); + log("Unable to create logging path"); return; } // Open file handle to log file of current date QString fileName = directory + QDir::separator() + baseFileName; - Log("Logging to {}", fileName); + log("Logging to {}", fileName); this->fileHandle.setFileName(fileName); this->fileHandle.open(QIODevice::Append); diff --git a/src/util/FormatTime.cpp b/src/util/FormatTime.cpp index 2eccca901..63b2e3c6b 100644 --- a/src/util/FormatTime.cpp +++ b/src/util/FormatTime.cpp @@ -2,11 +2,11 @@ namespace chatterino { namespace { -void appendDuration(int count, QChar &&order, QString &outString) -{ - outString.append(QString::number(count)); - outString.append(order); -} + void appendDuration(int count, QChar &&order, QString &outString) + { + outString.append(QString::number(count)); + outString.append(order); + } } // namespace QString formatTime(int totalSeconds) diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index b708728b1..bb6c742a2 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -6,7 +6,7 @@ namespace chatterino { template -auto fS(Args &&... args) -> decltype(fmt::format(std::forward(args)...)) +auto fS(Args &&... args) { return fmt::format(std::forward(args)...); } @@ -17,6 +17,43 @@ static QString CreateUUID() return uuid.toString(); } +static QString createLink(const QString &url, bool file = false) +{ + if (file) { + return QString("" + url + + ""); + } + + return QString("" + + url + ""); +} + +static QString createNamedLink(const QString &url, const QString &name, bool file = false) +{ + if (file) { + return QString("" + name + + ""); + } + + return QString("" + + name + ""); +} + +static QString shortenString(const QString &str, unsigned maxWidth = 50) +{ + if (str.size() <= maxWidth) { + return str; + } + + QString shortenedStr = str; + shortenedStr.resize(47); + shortenedStr += "..."; + + return shortenedStr; +} + } // namespace chatterino namespace fmt { diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index d10ba2bc1..54f06976d 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -1,20 +1,17 @@ #include "InitUpdateButton.hpp" #include "widgets/dialogs/UpdateDialog.hpp" -#include "widgets/helper/RippleEffectButton.hpp" +#include "widgets/helper/Button.hpp" namespace chatterino { -void initUpdateButton(RippleEffectButton &button, - std::unique_ptr &handle, +void initUpdateButton(Button &button, pajlada::Signals::SignalHolder &signalHolder) { button.hide(); // show update prompt when clicking the button - QObject::connect(&button, &RippleEffectButton::clicked, [&button, &handle] { - (void)(handle); - + QObject::connect(&button, &Button::clicked, [&button] { auto dialog = new UpdateDialog(); dialog->setActionOnFocusLoss(BaseWindow::Delete); dialog->move(button.mapToGlobal( diff --git a/src/util/InitUpdateButton.hpp b/src/util/InitUpdateButton.hpp index 17237d593..c8caaf204 100644 --- a/src/util/InitUpdateButton.hpp +++ b/src/util/InitUpdateButton.hpp @@ -4,17 +4,16 @@ namespace pajlada { namespace Signals { -class SignalHolder; + class SignalHolder; } } // namespace pajlada namespace chatterino { -class RippleEffectButton; +class Button; class UpdateDialog; -void initUpdateButton(RippleEffectButton &button, - std::unique_ptr &handle, +void initUpdateButton(Button &button, pajlada::Signals::SignalHolder &signalHolder); } // namespace chatterino diff --git a/src/util/LayoutHelper.hpp b/src/util/LayoutHelper.hpp new file mode 100644 index 000000000..b996aa084 --- /dev/null +++ b/src/util/LayoutHelper.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace chatterino { + +using LayoutItem = boost::variant; + +template +T *makeLayout(std::initializer_list items) +{ + auto t = new T; + + for (auto &item : items) { + switch (item.which()) { + case 0: + t->addItem(new QWidgetItem(boost::get(item))); + break; + case 1: + t->addItem(boost::get(item)); + break; + } + } + + return t; +} + +template +T *makeWidget(With with) +{ + auto t = new T; + + with(t); + + return t; +} + +} // namespace chatterino diff --git a/src/util/RapidJsonSerializeQString.hpp b/src/util/RapidJsonSerializeQString.hpp new file mode 100644 index 000000000..0d34a256b --- /dev/null +++ b/src/util/RapidJsonSerializeQString.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +namespace pajlada { +namespace Settings { + + template <> + struct Serialize { + static rapidjson::Value get(const QString &value, + rapidjson::Document::AllocatorType &a) + { + return rapidjson::Value(value.toUtf8(), a); + } + }; + + template <> + struct Deserialize { + static QString get(const rapidjson::Value &value, bool *error = nullptr) + { + if (!value.IsString()) { + PAJLADA_REPORT_ERROR(error) + PAJLADA_THROW_EXCEPTION( + "Deserialized rapidjson::Value is not a string"); + return QString{}; + } + + try { + return QString::fromUtf8(value.GetString(), + value.GetStringLength()); + } catch (const std::exception &) { + // int x = 5; + } catch (...) { + // int y = 5; + } + + return QString{}; + } + }; + +} // namespace Settings +} // namespace pajlada diff --git a/src/util/RapidjsonHelpers.cpp b/src/util/RapidjsonHelpers.cpp index 78256b4a2..2a9742df2 100644 --- a/src/util/RapidjsonHelpers.cpp +++ b/src/util/RapidjsonHelpers.cpp @@ -5,26 +5,28 @@ namespace chatterino { namespace rj { -void addMember(rapidjson::Value &obj, const char *key, rapidjson::Value &&value, - rapidjson::Document::AllocatorType &a) -{ - obj.AddMember(rapidjson::Value(key, a).Move(), value, a); -} + void addMember(rapidjson::Value &obj, const char *key, + rapidjson::Value &&value, + rapidjson::Document::AllocatorType &a) + { + obj.AddMember(rapidjson::Value(key, a).Move(), value, a); + } -void addMember(rapidjson::Value &obj, const char *key, rapidjson::Value &value, - rapidjson::Document::AllocatorType &a) -{ - obj.AddMember(rapidjson::Value(key, a).Move(), value.Move(), a); -} + void addMember(rapidjson::Value &obj, const char *key, + rapidjson::Value &value, + rapidjson::Document::AllocatorType &a) + { + obj.AddMember(rapidjson::Value(key, a).Move(), value.Move(), a); + } -std::string stringify(const rapidjson::Value &value) -{ - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - value.Accept(writer); + std::string stringify(const rapidjson::Value &value) + { + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + value.Accept(writer); - return std::string(buffer.GetString()); -} + return std::string(buffer.GetString()); + } } // namespace rj } // namespace chatterino diff --git a/src/util/RapidjsonHelpers.hpp b/src/util/RapidjsonHelpers.hpp index db6db99f4..d3e73e8f9 100644 --- a/src/util/RapidjsonHelpers.hpp +++ b/src/util/RapidjsonHelpers.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include #include @@ -11,91 +11,95 @@ namespace chatterino { namespace rj { -void addMember(rapidjson::Value &obj, const char *key, rapidjson::Value &&value, - rapidjson::Document::AllocatorType &a); -void addMember(rapidjson::Value &obj, const char *key, rapidjson::Value &value, - rapidjson::Document::AllocatorType &a); + void addMember(rapidjson::Value &obj, const char *key, + rapidjson::Value &&value, + rapidjson::Document::AllocatorType &a); + void addMember(rapidjson::Value &obj, const char *key, + rapidjson::Value &value, + rapidjson::Document::AllocatorType &a); -template -void set(rapidjson::Value &obj, const char *key, const Type &value, - rapidjson::Document::AllocatorType &a) -{ - assert(obj.IsObject()); + template + void set(rapidjson::Value &obj, const char *key, const Type &value, + rapidjson::Document::AllocatorType &a) + { + assert(obj.IsObject()); - addMember(obj, key, pajlada::Settings::Serialize::get(value, a), a); -} - -template <> -inline void set(rapidjson::Value &obj, const char *key, - const rapidjson::Value &value, - rapidjson::Document::AllocatorType &a) -{ - assert(obj.IsObject()); - - addMember(obj, key, const_cast(value), a); -} - -template -void set(rapidjson::Document &obj, const char *key, const Type &value) -{ - assert(obj.IsObject()); - - auto &a = obj.GetAllocator(); - - addMember(obj, key, pajlada::Settings::Serialize::get(value, a), a); -} - -template <> -inline void set(rapidjson::Document &obj, const char *key, - const rapidjson::Value &value) -{ - assert(obj.IsObject()); - - auto &a = obj.GetAllocator(); - - addMember(obj, key, const_cast(value), a); -} - -template -void add(rapidjson::Value &arr, const Type &value, - rapidjson::Document::AllocatorType &a) -{ - assert(arr.IsArray()); - - arr.PushBack(pajlada::Settings::Serialize::get(value, a), a); -} - -template -bool getSafe(const rapidjson::Value &obj, const char *key, Type &out) -{ - if (!obj.IsObject()) { - return false; + addMember(obj, key, pajlada::Settings::Serialize::get(value, a), + a); } - if (!obj.HasMember(key)) { - return false; + template <> + inline void set(rapidjson::Value &obj, const char *key, + const rapidjson::Value &value, + rapidjson::Document::AllocatorType &a) + { + assert(obj.IsObject()); + + addMember(obj, key, const_cast(value), a); } - if (obj.IsNull()) { - return false; + template + void set(rapidjson::Document &obj, const char *key, const Type &value) + { + assert(obj.IsObject()); + + auto &a = obj.GetAllocator(); + + addMember(obj, key, pajlada::Settings::Serialize::get(value, a), + a); } - bool error = false; - out = pajlada::Settings::Deserialize::get(obj[key], &error); + template <> + inline void set(rapidjson::Document &obj, const char *key, + const rapidjson::Value &value) + { + assert(obj.IsObject()); - return !error; -} + auto &a = obj.GetAllocator(); -template -bool getSafe(const rapidjson::Value &value, Type &out) -{ - bool error = false; - out = pajlada::Settings::Deserialize::get(value, &error); + addMember(obj, key, const_cast(value), a); + } - return !error; -} + template + void add(rapidjson::Value &arr, const Type &value, + rapidjson::Document::AllocatorType &a) + { + assert(arr.IsArray()); -std::string stringify(const rapidjson::Value &value); + arr.PushBack(pajlada::Settings::Serialize::get(value, a), a); + } + + template + bool getSafe(const rapidjson::Value &obj, const char *key, Type &out) + { + if (!obj.IsObject()) { + return false; + } + + if (!obj.HasMember(key)) { + return false; + } + + if (obj.IsNull()) { + return false; + } + + bool error = false; + out = pajlada::Settings::Deserialize::get(obj[key], &error); + + return !error; + } + + template + bool getSafe(const rapidjson::Value &value, Type &out) + { + bool error = false; + out = pajlada::Settings::Deserialize::get(value, &error); + + return !error; + } + + std::string stringify(const rapidjson::Value &value); } // namespace rj } // namespace chatterino diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index 2fd2ef0a3..972c47f59 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "Helpers.hpp" +#include "debug/Log.hpp" #include "singletons/Settings.hpp" #include "widgets/dialogs/QualityPopup.hpp" @@ -15,88 +16,89 @@ namespace chatterino { namespace { -const char *getBinaryName() -{ + const char *getBinaryName() + { #ifdef _WIN32 - return "streamlink.exe"; + return "streamlink.exe"; #else - return "streamlink"; + return "streamlink"; #endif -} + } -const char *getDefaultBinaryPath() -{ + const char *getDefaultBinaryPath() + { #ifdef _WIN32 - return "C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe"; + return "C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe"; #else - return "/usr/bin/streamlink"; + return "/usr/bin/streamlink"; #endif -} - -QString getStreamlinkProgram() -{ - auto app = getApp(); - - if (app->settings->streamlinkUseCustomPath) { - return app->settings->streamlinkPath + "/" + getBinaryName(); - } else { - return getBinaryName(); - } -} - -bool checkStreamlinkPath(const QString &path) -{ - QFileInfo fileinfo(path); - - if (!fileinfo.exists()) { - return false; - // throw Exception(fS("Streamlink path ({}) is invalid, file does not - // exist", path)); } - return fileinfo.isExecutable(); -} + QString getStreamlinkProgram() + { + auto app = getApp(); -void showStreamlinkNotFoundError() -{ - static QErrorMessage *msg = new QErrorMessage; - - auto app = getApp(); - if (app->settings->streamlinkUseCustomPath) { - msg->showMessage( - "Unable to find Streamlink executable\nMake sure your custom path " - "is pointing " - "to the DIRECTORY where the streamlink executable is located"); - } else { - msg->showMessage( - "Unable to find Streamlink executable.\nIf you have Streamlink " - "installed, you might need to enable the custom path option"); - } -} - -QProcess *createStreamlinkProcess() -{ - auto p = new QProcess; - p->setProgram(getStreamlinkProgram()); - - QObject::connect(p, &QProcess::errorOccurred, [=](auto err) { - if (err == QProcess::FailedToStart) { - showStreamlinkNotFoundError(); + if (getSettings()->streamlinkUseCustomPath) { + return getSettings()->streamlinkPath + "/" + getBinaryName(); } else { - qDebug() << "Error occured: " << err; // + return getBinaryName(); + } + } + + bool checkStreamlinkPath(const QString &path) + { + QFileInfo fileinfo(path); + + if (!fileinfo.exists()) { + return false; + // throw Exception(fS("Streamlink path ({}) is invalid, file does + // not exist", path)); } - p->deleteLater(); - }); + return fileinfo.isExecutable(); + } - QObject::connect(p, - static_cast(&QProcess::finished), - [=](int res) { - p->deleteLater(); // - }); + void showStreamlinkNotFoundError() + { + static QErrorMessage *msg = new QErrorMessage; - return p; -} + auto app = getApp(); + if (getSettings()->streamlinkUseCustomPath) { + msg->showMessage( + "Unable to find Streamlink executable\nMake sure your custom " + "path " + "is pointing " + "to the DIRECTORY where the streamlink executable is located"); + } else { + msg->showMessage( + "Unable to find Streamlink executable.\nIf you have Streamlink " + "installed, you might need to enable the custom path option"); + } + } + + QProcess *createStreamlinkProcess() + { + auto p = new QProcess; + p->setProgram(getStreamlinkProgram()); + + QObject::connect(p, &QProcess::errorOccurred, [=](auto err) { + if (err == QProcess::FailedToStart) { + showStreamlinkNotFoundError(); + } else { + log("Error occured {}", err); + } + + p->deleteLater(); + }); + + QObject::connect( + p, static_cast(&QProcess::finished), + [=](int res) { + p->deleteLater(); // + }); + + return p; + } } // namespace @@ -109,7 +111,7 @@ void getStreamQualities(const QString &channelURL, p, static_cast(&QProcess::finished), [=](int res) { if (res != 0) { - qDebug() << "Got error code" << res; + log("Got error code {}", res); // return; } QString lastLine = QString(p->readAllStandardOutput()); @@ -121,7 +123,15 @@ void getStreamQualities(const QString &channelURL, for (int i = split.length() - 1; i >= 0; i--) { QString option = split.at(i); - if (option.endsWith(" (worst)")) { + if (option == "best)") { + // As it turns out, sometimes, one quality option can + // be the best and worst quality at the same time. + // Since we start loop from the end, we can check + // that and act accordingly + option = split.at(--i); + // "900p60 (worst" + options << option.left(option.length() - 7); + } else if (option.endsWith(" (worst)")) { options << option.left(option.length() - 8); } else if (option.endsWith(" (best)")) { options << option.left(option.length() - 7); @@ -146,9 +156,9 @@ void openStreamlink(const QString &channelURL, const QString &quality, QStringList arguments; - QString additionalOptions = app->settings->streamlinkOpts.getValue(); + QString additionalOptions = getSettings()->streamlinkOpts.getValue(); if (!additionalOptions.isEmpty()) { - arguments << app->settings->streamlinkOpts; + arguments << getSettings()->streamlinkOpts; } arguments.append(extraArguments); @@ -173,7 +183,7 @@ void openStreamlinkForChannel(const QString &channel) QString channelURL = "twitch.tv/" + channel; - QString preferredQuality = app->settings->preferredQuality; + QString preferredQuality = getSettings()->preferredQuality; preferredQuality = preferredQuality.toLower(); if (preferredQuality == "choose") { diff --git a/src/util/WindowsHelper.hpp b/src/util/WindowsHelper.hpp index fdbafa9c2..f02bb1362 100644 --- a/src/util/WindowsHelper.hpp +++ b/src/util/WindowsHelper.hpp @@ -2,8 +2,8 @@ #ifdef USEWINSDK -#include -#include +# include +# include namespace chatterino { diff --git a/src/widgets/AccountSwitchWidget.cpp b/src/widgets/AccountSwitchWidget.cpp index 9c307ba73..3e201ded3 100644 --- a/src/widgets/AccountSwitchWidget.cpp +++ b/src/widgets/AccountSwitchWidget.cpp @@ -3,6 +3,7 @@ #include "Application.hpp" #include "common/Common.hpp" #include "controllers/accounts/AccountController.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" namespace chatterino { diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index b247129b6..fee940591 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -8,12 +8,12 @@ #include #ifdef USEWINSDK -#include "util/WindowsHelper.hpp" +# include "util/WindowsHelper.hpp" -#include "Windows.h" +# include "Windows.h" // don't even think about reordering these -#include "Psapi.h" -#pragma comment(lib, "Dwmapi.lib") +# include "Psapi.h" +# pragma comment(lib, "Dwmapi.lib") #endif namespace chatterino { diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index 80b030fca..64da52f6a 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -1,12 +1,14 @@ #pragma once +#include #include -#include "common/Channel.hpp" -#include "widgets/splits/Split.hpp" - namespace chatterino { +class Split; +class Channel; +using ChannelPtr = std::shared_ptr; + class AttachedWindow : public QWidget { AttachedWindow(void *_target, int _yOffset); diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 424b96e6d..6394284e0 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -4,12 +4,13 @@ #include "boost/algorithm/algorithm.hpp" #include "debug/Log.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/PostToThread.hpp" #include "util/WindowsHelper.hpp" #include "widgets/Label.hpp" #include "widgets/TooltipWidget.hpp" -#include "widgets/helper/RippleEffectLabel.hpp" +#include "widgets/helper/EffectLabel.hpp" #include "widgets/helper/Shortcut.hpp" #include @@ -19,20 +20,20 @@ #include #ifdef USEWINSDK -#include -#include -#include -#include -#include -#include +# include +# include +# include +# include +# include +# include //#include -#pragma comment(lib, "Dwmapi.lib") +# pragma comment(lib, "Dwmapi.lib") -#include -#include +# include +# include -#define WM_DPICHANGED 0x02E0 +# define WM_DPICHANGED 0x02E0 #endif #include "widgets/helper/TitlebarButton.hpp" @@ -55,13 +56,13 @@ BaseWindow::BaseWindow(QWidget *parent, Flags _flags) this->init(); this->connections_.managedConnect( - getApp()->settings->uiScale.getValueChangedSignal(), + getSettings()->uiScale.getValueChangedSignal(), [this](auto, auto) { postToThread([this] { this->updateScale(); }); }); this->updateScale(); createWindowShortcut(this, "CTRL+0", - [] { getApp()->settings->uiScale.setValue(0); }); + [] { getSettings()->uiScale.setValue(0); }); // QTimer::this->scaleChangedEvent(this->getScale()); } @@ -104,19 +105,17 @@ void BaseWindow::init() QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); policy.setHorizontalStretch(1); - // title->setBaseSize(0, 0); - // title->setScaledContents(true); title->setSizePolicy(policy); buttonLayout->addWidget(title); this->ui_.titleLabel = title; // buttons TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButton::Minimize); + _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButton::Maximize); + _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButton::Close); + _exitButton->setButtonStyle(TitleBarButtonStyle::Close); QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { @@ -164,7 +163,7 @@ void BaseWindow::init() // fourtf: don't ask me why we need to delay this if (!(this->flags_ & Flags::TopMost)) { QTimer::singleShot(1, this, [this] { - getApp()->settings->windowTopMost.connect( + getSettings()->windowTopMost.connect( [this](bool topMost, auto) { ::SetWindowPos(HWND(this->winId()), topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, @@ -175,7 +174,7 @@ void BaseWindow::init() }); } #else -// if (getApp()->settings->windowTopMost.getValue()) { +// if (getSettings()->windowTopMost.getValue()) { // this->setWindowFlag(Qt::WindowStaysOnTopHint); // } #endif @@ -239,7 +238,7 @@ void BaseWindow::themeChangedEvent() this->ui_.titleLabel->setPalette(palette_title); } - for (RippleEffectButton *button : this->ui_.buttons) { + for (Button *button : this->ui_.buttons) { button->setMouseEffectColor(this->theme->window.text); } } else { @@ -268,11 +267,11 @@ void BaseWindow::wheelEvent(QWheelEvent *event) if (event->modifiers() & Qt::ControlModifier) { if (event->delta() > 0) { - getApp()->settings->uiScale.setValue(WindowManager::clampUiScale( - getApp()->settings->uiScale.getValue() + 1)); + getSettings()->uiScale.setValue(WindowManager::clampUiScale( + getSettings()->uiScale.getValue() + 1)); } else { - getApp()->settings->uiScale.setValue(WindowManager::clampUiScale( - getApp()->settings->uiScale.getValue() - 1)); + getSettings()->uiScale.setValue(WindowManager::clampUiScale( + getSettings()->uiScale.getValue() - 1)); } } } @@ -317,7 +316,7 @@ void BaseWindow::mousePressEvent(QMouseEvent *event) }; if (!recursiveCheckMouseTracking(widget)) { - Log("Start moving"); + log("Start moving"); this->moving = true; } } @@ -332,7 +331,7 @@ void BaseWindow::mouseReleaseEvent(QMouseEvent *event) #ifndef Q_OS_WIN if (this->flags_ & FramelessDraggable) { if (this->moving) { - Log("Stop moving"); + log("Stop moving"); this->moving = false; } } @@ -355,8 +354,8 @@ void BaseWindow::mouseMoveEvent(QMouseEvent *event) BaseWidget::mouseMoveEvent(event); } -TitleBarButton *BaseWindow::addTitleBarButton( - const TitleBarButton::Style &style, std::function onClicked) +TitleBarButton *BaseWindow::addTitleBarButton(const TitleBarButtonStyle &style, + std::function onClicked) { TitleBarButton *button = new TitleBarButton; button->setScaleIndependantSize(30, 30); @@ -371,15 +370,15 @@ TitleBarButton *BaseWindow::addTitleBarButton( return button; } -RippleEffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) +EffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) { - RippleEffectLabel *button = new RippleEffectLabel; + EffectLabel *button = new EffectLabel; button->setScaleIndependantHeight(30); this->ui_.buttons.push_back(button); this->ui_.titlebarBox->insertWidget(1, button); - QObject::connect(button, &RippleEffectLabel::clicked, this, + QObject::connect(button, &EffectLabel::clicked, this, [onClicked] { onClicked(); }); return button; @@ -391,10 +390,10 @@ void BaseWindow::changeEvent(QEvent *) #ifdef USEWINSDK if (this->ui_.maxButton) { - this->ui_.maxButton->setButtonStyle(this->windowState() & - Qt::WindowMaximized - ? TitleBarButton::Unmaximize - : TitleBarButton::Maximize); + this->ui_.maxButton->setButtonStyle( + this->windowState() & Qt::WindowMaximized + ? TitleBarButtonStyle::Unmaximize + : TitleBarButtonStyle::Maximize); } #endif @@ -461,7 +460,11 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) { #ifdef USEWINSDK +# if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1)) + MSG *msg = *reinterpret_cast(message); +# else MSG *msg = reinterpret_cast(message); +# endif bool returnValue = false; diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index dbfb4b46b..088c07c86 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -1,7 +1,6 @@ #pragma once #include "BaseWidget.hpp" -#include "widgets/helper/TitlebarButton.hpp" #include #include @@ -12,9 +11,10 @@ typedef struct tagMSG MSG; namespace chatterino { -class RippleEffectButton; -class RippleEffectLabel; +class Button; +class EffectLabel; class TitleBarButton; +enum class TitleBarButtonStyle; class BaseWindow : public BaseWidget { @@ -36,9 +36,9 @@ public: QWidget *getLayoutContainer(); bool hasCustomWindowFrame(); - TitleBarButton *addTitleBarButton(const TitleBarButton::Style &style, + TitleBarButton *addTitleBarButton(const TitleBarButtonStyle &style, std::function onClicked); - RippleEffectLabel *addTitleBarLabel(std::function onClicked); + EffectLabel *addTitleBarLabel(std::function onClicked); void setStayInScreenRect(bool value); bool getStayInScreenRect() const; @@ -109,11 +109,11 @@ private: TitleBarButton *maxButton = nullptr; TitleBarButton *exitButton = nullptr; QWidget *layoutBase = nullptr; - std::vector buttons; + std::vector