diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..8ac1ca177 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Suggestions or feature request + url: https://github.com/chatterino/chatterino2/discussions/categories/ideas + about: Got something you think should change or be added? Search for or start a new discussion! + - name: Help + url: https://github.com/chatterino/chatterino2/discussions/categories/q-a + about: Chatterino2 not working as you'd expect? Not sure it's a bug? Check the Q&A section! diff --git a/.gitmodules b/.gitmodules index c0212bf22..adef9ede4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "lib/libcommuni"] path = lib/libcommuni url = https://github.com/Chatterino/libcommuni + branch = chatterino-cmake [submodule "lib/qBreakpad"] path = lib/qBreakpad url = https://github.com/jiakuan/qBreakpad.git diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md index 74098237c..25f17f8b9 100644 --- a/BUILDING_ON_FREEBSD.md +++ b/BUILDING_ON_FREEBSD.md @@ -10,6 +10,7 @@ FreeBSD 13.0-CURRENT. 1. Install build dependencies from package sources (or build from the ports tree): `# pkg install qt5-core qt5-multimedia qt5-svg qt5-qmake qt5-buildtools gstreamer-plugins-good boost-libs rapidjson` -1. go into project directory -1. create build folder `$ mkdir build && cd build` -1. `$ qmake .. && make` +1. Go into the project directory +1. Create a build folder and go into it (`mkdir build && cd build`) +1. Proceed to compiling using the command + `qmake .. && make` diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 401741a27..d9df2848b 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -4,27 +4,28 @@ Note on Qt version compatibility: If you are installing Qt from a package manage ## Ubuntu 18.04 -_most likely works the same for other Debian-like distros_ +_Most likely works the same for other Debian-like distros_ -1. Install dependencies `sudo apt install qttools5-dev qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++` +1. Install all of the dependencies using `sudo apt install qttools5-dev qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++` -### Through Qt Creator +### Compiling through Qt Creator -1. Install C++ IDE Qt Creator `sudo apt install qtcreator` +1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` 1. Open `chatterino.pro` with Qt Creator and select build ### Manually -1. go into project directory -1. create build folder `mkdir build && cd build` +1. Go into the project directory +1. Create a build folder and go into it (`mkdir build && cd build`) +1. Use one of the options below to compile it -#### Using QMake +### Using CMake -1. `qmake .. && make` +`cmake .. && make` -#### Using CMake +### Using QMake -1. `cmake .. && make` +`qmake .. && make` ## Arch Linux @@ -34,50 +35,47 @@ _most likely works the same for other Debian-like distros_ ### Manually -1. `sudo pacman -S qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake` -1. go into project directory -1. create build folder `mkdir build && cd build` +1. Install all of the dependencies using `sudo pacman -S qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake` +1. Go into the project directory +1. Create a build folder and go into it (`mkdir build && cd build`) +1. Use one of the options below to compile it -#### Using QMake +### Using CMake -1. `qmake .. && make` +`cmake .. && make` -#### Using CMake +### Using QMake -1. `cmake .. && make` +`qmake .. && make` ## Fedora 28 and above -_most likely works the same for other Red Hat-like distros. Substitue `dnf` with `yum`._ +_Most likely works the same for other Red Hat-like distros. Substitute `dnf` with `yum`._ -1. `sudo dnf install qt5-qtbase-devel qt5-qtmultimedia-devel qt5-qtsvg-devel libsecret-devel openssl-devel boost-devel cmake` -1. go into project directory -1. create build folder `mkdir build && cd build` - -### Using QMake - -1. `qmake-qt5 .. && make -j$(nproc)` +1. Install all of the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtmultimedia-devel qt5-qtsvg-devel libsecret-devel openssl-devel boost-devel cmake` +1. Go into the project directory +1. Create a build folder and go into it (`mkdir build && cd build`) +1. Use one of the options below to compile it ### Using CMake -1. `cmake .. && make -j$(nproc)` +`cmake .. && make -j$(nproc)` -### Optional dependencies +### Using QMake -_`gstreamer-plugins-good` package is retired in Fedora 31, see: [rhbz#1735324](https://bugzilla.redhat.com/show_bug.cgi?id=1735324)_ - -1. `sudo dnf install gstreamer-plugins-good` _(optional: for audio output)_ +`qmake-qt5 .. && make -j$(nproc)` ## NixOS 18.09+ -1. enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake` -1. go into project directory -1. create build folder `mkdir build && cd build` - -### Using QMake - -1. `qmake .. && make` +1. Enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake` +1. Go into the project directory +1. Create a build folder and go into it (`mkdir build && cd build`) +1. Use one of the options below to compile it ### Using CMake -1. `cmake .. && make` +`cmake .. && make` + +### Using QMake + +`qmake .. && make` diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index 3a6bcb37d..5d4182187 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -4,15 +4,15 @@ #### Note - Chatterino 2 is only tested on macOS 10.14 and above - anything below that is considered unsupported. It may or may not work on earlier versions -1. Install Xcode and Xcode Command Line Utilites -2. Start Xcode, settings -> Locations, activate your Command Line Tools +1. Install Xcode and Xcode Command Line Utilities +2. Start Xcode, go into Settings -> Locations, and activate your Command Line Tools 3. Install brew https://brew.sh/ -4. `brew install boost openssl rapidjson` -5. `brew install qt` -6. Step 5 should output some directions to add qt to your path, you will need to do this for qmake -7. Go into project directory -8. Create build folder `mkdir build && cd build` -9. `qmake .. && make` +4. Install the dependencies using `brew install boost openssl rapidjson` +5. Install Qt using `brew install qt` +6. Step 5 should output some directions to add Qt to your path, you will need to do this for qmake +7. Go into the project directory +8. Create a build folder and go into it (`mkdir build && cd build`) +9. Compile using `qmake .. && make` _If you want to use cmake instead of qmake, just replace the above qmake command with cmake_ @@ -21,7 +21,7 @@ If the Project does not build at this point, you might need to add additional Pa `brew info openssl` `brew info boost` -If brew doesn't link openssl properly then you should be able to link it yourself using those two commands: +If brew doesn't link OpenSSL properly then you should be able to link it yourself by using these two commands: - `ln -s /usr/local/opt/openssl/lib/* /usr/local/lib` - `ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl` diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index f46387614..a075761f8 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -1,8 +1,8 @@ # Building on Windows -**Note that installing all the development prerequisites and libraries will require about 30 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.** +**Note that installing all of the development prerequisites and libraries will require about 30 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.** -This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build chatterino on a 32-bit system. +This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build Chatterino on a 32-bit system. ## Visual Studio 2019 @@ -39,12 +39,12 @@ Note: This installation will take about 1.5 GB of disk space. ### For Qt SSL, we need OpenSSL 1.0 -1. Download OpenSSL for windows, version `1.0.2u`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)** +1. Download OpenSSL for Windows, version `1.0.2u`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)** 2. When prompted, install it to any arbitrary empty directory. 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) 5. Then copy the OpenSSL 1.1 files from its `\bin` folder to `C:\local\bin` (Overwrite any duplicate files) -6. Add `C:\local\bin` to your path folder ([Follow guide here if you don't know how to do it](https://www.computerhope.com/issues/ch000549.htm#windows10)) +6. Add `C:\local\bin` to your path folder ([Follow the guide here if you don't know how to do it](https://www.computerhope.com/issues/ch000549.htm#windows10)) **If the download links above do not work, try downloading similar 1.1.x & 1.0.x versions [here](https://slproweb.com/products/Win32OpenSSL.html). Note: Don't download the "light" installers, they do not have the required files.** @@ -82,7 +82,7 @@ Compiling with Breakpad support enables crash reports that can be of use for dev ## Run the build in Qt Creator -1. Open the `chatterino.pro` file by double-clicking or by opening it via Qt Creator. +1. Open the `chatterino.pro` file by double-clicking it, or by opening it via Qt Creator. 2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this: ![Qt Create Configure Project screenshot](https://i.imgur.com/dbz45mB.png) 3. Select the profile(s) you want to build with and click "Configure Project". @@ -105,10 +105,10 @@ To produce a standalone package, you need to generate all required files using t To produce all supplement files for a standalone build, follow these steps (adjust paths as required): -1. Navigate to your build output directory with windows explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release` +1. Navigate to your build output directory with Windows Explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release` 2. Enter the `release` directory 3. Delete all files except the `chatterino.exe` file. You should be left with a directory only containing `chatterino.exe`. -4. Open a `cmd` window and execute: +4. Open a command prompt and execute: cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release\release C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe chatterino.exe @@ -124,9 +124,9 @@ To produce all supplement files for a standalone build, follow these steps (adju You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)). -## Building with CMake +## Using CMake -Open up your terminal with the Visual Studio environment variables, then: +Open up your terminal with the Visual Studio environment variables, then enter the following commands: 1. `mkdir build` 2. `cd build` diff --git a/CHANGELOG.md b/CHANGELOG.md index 871fe4326..51edeff42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,34 @@ ## Unversioned - Major: Changed login process. It is now fully automatic with no need to copy-paste credentials and is fully client-sided (doesn't require connecting to chatterino.com). (#3065) +- Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136) +- Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143) +- Minor: Display a system message when reloading subscription emotes to match BTTV/FFZ behavior (#3135) +- Bugfix: Moderation mode and active filters are now preserved when opening a split as a popup. (#3113, #3130) +- Bugfix: Fixed a bug that caused all badge highlights to use the same color. (#3132, #3134) +- Dev: Renamed CMake's build option `USE_SYSTEM_QT5KEYCHAIN` to `USE_SYSTEM_QTKEYCHAIN`. (#3103) +- Dev: Add benchmarks that can be compiled with the `BUILD_BENCHMARKS` CMake flag. Off by default. (#3038) + +## 2.3.4 + - Major: Newly uploaded Twitch emotes are once again present in emote picker and can be autocompleted with Tab as well. (#2992) +- Major: Deprecated `/(un)follow` commands and (un)following in the usercards as Twitch has removed this feature for 3rd party applications. (#3076, #3078) +- Major: Added the ability to add nicknames for users. (#137, #2981) +- Major: Fixed constant disconnections with more than 20 channels by rate-limiting outgoing JOIN messages. (#3112, #3115) - Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999, #3033) - Minor: Received Twitch messages now use the exact same timestamp (obtained from Twitch's server) for every Chatterino user instead of assuming message timestamp on client's side. (#3021) - Minor: Received IRC messages use `time` message tag for timestamp if it's available. (#3021) - Minor: Added informative messages for recent-messages API's errors. (#3029) +- Minor: Added section with helpful Chatterino-related links to the About page. (#3068) +- Minor: Now uses spaces instead of magic Unicode character for sending duplicate messages (#3081) +- Minor: Added `channel.live` filter variable (#3092, #3110) - Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010) +- Bugfix: Fixed PubSub not properly trying to resolve pending listens when the pending listens list was larger than 50. (#3037) +- Bugfix: Copy buttons in usercard now show properly in light mode (#3057) - Bugfix: Fixed comma appended to username completion when not at the beginning of the message. (#3060) +- Bugfix: Fixed bug misplacing chat when zooming on Chrome with Chatterino Native Host extension (#1936) +- Bugfix: Channel point redemptions from ignored users are now properly blocked. (#3102) +- Dev: Allow building against Qt 5.11 (#3105) - Dev: Ubuntu packages are now available (#2936) - Dev: Disabled update checker on Flatpak. (#3051) - Dev: Add logging for HTTP requests (#2991) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ba991137..d1723b414 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,17 +7,25 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" ) -project(chatterino VERSION 2.3.3) +project(chatterino VERSION 2.3.4) option(BUILD_APP "Build Chatterino" ON) option(BUILD_TESTS "Build the tests for Chatterino" OFF) +option(BUILD_BENCHMARKS "Build the benchmarks for Chatterino" OFF) option(USE_SYSTEM_PAJLADA_SETTINGS "Use system pajlada settings library" OFF) option(USE_SYSTEM_LIBCOMMUNI "Use system communi library" OFF) -option(USE_SYSTEM_QT5KEYCHAIN "Use system Qt5Keychain library" OFF) +option(USE_SYSTEM_QTKEYCHAIN "Use system QtKeychain library" OFF) option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) +option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF) option(USE_CONAN "Use conan" OFF) +if (BUILD_WITH_QT6) + set(MAJOR_QT_VERSION "6") +else() + set(MAJOR_QT_VERSION "5") +endif() + if (USE_CONAN OR CONAN_EXPORTED) include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake) conan_basic_setup(TARGETS NO_OUTPUT_DIRS) @@ -31,7 +39,7 @@ endif () include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake) -find_package(Qt5 REQUIRED +find_package(Qt${MAJOR_QT_VERSION} REQUIRED COMPONENTS Core Widgets @@ -73,17 +81,17 @@ endif() # Link QtKeychain statically option(QTKEYCHAIN_STATIC "" ON) -if (USE_SYSTEM_QT5KEYCHAIN) - find_package(Qt5Keychain REQUIRED) +if (USE_SYSTEM_QTKEYCHAIN) + find_package(Qt${MAJOR_QT_VERSION}Keychain REQUIRED) else() - set(QT5KEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain") - if (NOT EXISTS "${QT5KEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt") + set(QTKEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain") + if (NOT EXISTS "${QTKEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt") message(FATAL_ERROR "Submodules probably not loaded, unable to find lib/qtkeychain/CMakeLists.txt") endif() - add_subdirectory("${QT5KEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL) - if (NOT TARGET qt5keychain) - message(FATAL_ERROR "qt5keychain target was not created :@") + add_subdirectory("${QTKEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL) + if (NOT TARGET qt${MAJOR_QT_VERSION}keychain) + message(FATAL_ERROR "qt${MAJOR_QT_VERSION}keychain target was not created :@") endif() endif() @@ -95,6 +103,11 @@ if (BUILD_TESTS) find_package(GTest REQUIRED) endif () +if (BUILD_BENCHMARKS) + # Include system benchmark (Google Benchmark) + find_package(benchmark REQUIRED) +endif () + find_package(PajladaSerialize REQUIRED) find_package(PajladaSignals REQUIRED) find_package(LRUCache REQUIRED) @@ -112,7 +125,7 @@ endif() set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if (BUILD_TESTS) +if (BUILD_TESTS OR BUILD_BENCHMARKS) add_definitions(-DCHATTERINO_TEST) endif () @@ -123,4 +136,8 @@ if (BUILD_TESTS) add_subdirectory(tests) endif () +if (BUILD_BENCHMARKS) + add_subdirectory(benchmarks) +endif () + feature_summary(WHAT ALL) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bd69a762..7135e908b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Chatterino code guidelines -This is a set of guidelines for contributing to Chatterino. The goal is to teach programmers without C++ background (java/python/etc.), people who haven't used Qt or otherwise have different experience the idioms of the codebase. Thus we will focus on those which are different from those other environments. There are extra guidelines available [here](https://hackmd.io/@fourtf/chatterino-pendantic-guidelines) but they are considered as extras and not as important. +This is a set of guidelines for contributing to Chatterino. The goal is to teach programmers without a C++ background (java/python/etc.), people who haven't used Qt, or otherwise have different experience, the idioms of the codebase. Thus we will focus on those which are different from those other environments. There are extra guidelines available [here](https://hackmd.io/@fourtf/chatterino-pendantic-guidelines) but they are considered as extras and not as important. # Tooling @@ -70,7 +70,7 @@ void myFunc() { ## Passing parameters -The way a parameter is passed signals how it is going to be used inside of the function. C++ doesn't have multiple return values so there is "out parameters" (reference to a variable that is going to be assigned inside of the function) to simulate multiple return values. +The way a parameter is passed, signals how it is going to be used inside of the function. C++ doesn't have multiple return values, so there are "out parameters" (reference to a variable that is going to be assigned inside of the function) to simulate multiple return values. **Cheap to copy types** like int/enum/etc. can be passed in per value since copying them is fast. @@ -122,11 +122,11 @@ void main() { } ``` -Generally the lowest level of requirement should be used e.g. passing `Channel&` instead of `std::shared_ptr&` (aka `ChannelPtr`) if possible. +Generally the lowest level of requirement should be used, e.g. passing `Channel&` instead of `std::shared_ptr&` (aka `ChannelPtr`) if possible. ## Members -All functions names are in `camelCase`. _Private_ member variables are in `camelCase_` (note the underscore at the end). We don't use the `get` prefix for getters. We mark functions as `const` [if applicable](https://stackoverflow.com/questions/751681/meaning-of-const-last-in-a-function-declaration-of-a-class). +All function names are in `camelCase`. _Private_ member variables are in `camelCase_` (note the underscore at the end). We don't use the `get` prefix for getters. We mark functions as `const` [if applicable](https://stackoverflow.com/questions/751681/meaning-of-const-last-in-a-function-declaration-of-a-class). ```cpp class NamedObject @@ -212,6 +212,6 @@ Keep the element on the stack if possible. If you need a pointer or have complex #### QObject classes -- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetime where possible. Objects are destroyed when their parent object is destroyed. +- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetimes where possible. Objects are destroyed when their parent object is destroyed. - If you have to explicitly delete an object use `variable->deleteLater()` instead of `delete variable`. This ensures that it will be deleted on the correct thread. -- If an object doesn't have a parent consider using `std::unique_ptr` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope or the object is destroyed. +- If an object doesn't have a parent, consider using `std::unique_ptr` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope, or the object is destroyed. diff --git a/benchmarks/.clang-format b/benchmarks/.clang-format new file mode 100644 index 000000000..f34c1465b --- /dev/null +++ b/benchmarks/.clang-format @@ -0,0 +1,35 @@ +Language: Cpp + +AccessModifierOffset: -4 +AlignEscapedNewlinesLeft: true +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakBeforeMultilineStrings: false +BasedOnStyle: Google +BraceWrapping: { + AfterClass: 'true' + AfterControlStatement: 'true' + AfterFunction: 'true' + AfterNamespace: 'false' + BeforeCatch: 'true' + BeforeElse: 'true' +} +BreakBeforeBraces: Custom +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +DerivePointerBinding: false +FixNamespaceComments: true +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +IndentPPDirectives: AfterHash +IncludeBlocks: Preserve +NamespaceIndentation: Inner +PointerBindsToType: false +SpacesBeforeTrailingComments: 2 +Standard: Auto +ReflowComments: false diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..6435f1398 --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,28 @@ +project(chatterino-benchmark) + +set(benchmark_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp + # Add your new file above this line! + ) + +add_executable(${PROJECT_NAME} ${benchmark_SOURCES}) +add_sanitizers(${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-lib) + +target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + CHATTERINO_TEST + ) + +set_target_properties(${PROJECT_NAME} + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin" + ) diff --git a/benchmarks/src/Emojis.cpp b/benchmarks/src/Emojis.cpp new file mode 100644 index 000000000..7eb5106e3 --- /dev/null +++ b/benchmarks/src/Emojis.cpp @@ -0,0 +1,57 @@ +#include "providers/emoji/Emojis.hpp" + +#include +#include +#include + +using namespace chatterino; + +static void BM_ShortcodeParsing(benchmark::State &state) +{ + Emojis emojis; + + emojis.load(); + + struct TestCase { + QString input; + QString expectedOutput; + }; + + std::vector tests{ + { + // input + "foo :penguin: bar", + // expected output + "foo 🐧 bar", + }, + { + // input + "foo :nonexistantcode: bar", + // expected output + "foo :nonexistantcode: bar", + }, + { + // input + ":male-doctor:", + // expected output + "👨‍⚕️", + }, + }; + + for (auto _ : state) + { + for (const auto &test : tests) + { + auto output = emojis.replaceShortCodes(test.input); + + auto matches = output == test.expectedOutput; + if (!matches && !output.endsWith(QChar(0xFE0F))) + { + // Try to append 0xFE0F if needed + output = output.append(QChar(0xFE0F)); + } + } + } +} + +BENCHMARK(BM_ShortcodeParsing); diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp new file mode 100644 index 000000000..501b3aa51 --- /dev/null +++ b/benchmarks/src/main.cpp @@ -0,0 +1,18 @@ +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + ::benchmark::Initialize(&argc, argv); + + QtConcurrent::run([&app] { + ::benchmark::RunSpecifiedBenchmarks(); + + app.exit(0); + }); + + return app.exec(); +} diff --git a/chatterino.pro b/chatterino.pro index 3eb7d382d..2d2e5f7d4 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -14,7 +14,7 @@ CCACHE_BIN = $$system(which ccache) CONFIG+=ccache } -MINIMUM_REQUIRED_QT_VERSION = 5.12.0 +MINIMUM_REQUIRED_QT_VERSION = 5.11.0 !versionAtLeast(QT_VERSION, $$MINIMUM_REQUIRED_QT_VERSION) { error("You're trying to compile with Qt $$QT_VERSION, but minimum required Qt version is $$MINIMUM_REQUIRED_QT_VERSION") @@ -159,9 +159,11 @@ SOURCES += \ src/controllers/highlights/HighlightModel.cpp \ src/controllers/highlights/HighlightPhrase.cpp \ src/controllers/highlights/UserHighlightModel.cpp \ + src/controllers/ignores/IgnoreController.cpp \ src/controllers/ignores/IgnoreModel.cpp \ src/controllers/moderationactions/ModerationAction.cpp \ src/controllers/moderationactions/ModerationActionModel.cpp \ + src/controllers/nicknames/NicknamesModel.cpp \ src/controllers/notifications/NotificationController.cpp \ src/controllers/notifications/NotificationModel.cpp \ src/controllers/pings/MutedChannelModel.cpp \ @@ -250,6 +252,7 @@ SOURCES += \ src/util/LayoutHelper.cpp \ src/util/NuulsUploader.cpp \ src/util/RapidjsonHelpers.cpp \ + src/util/RatelimitBucket.cpp \ src/util/SplitCommand.cpp \ src/util/StreamerMode.cpp \ src/util/StreamLink.cpp \ @@ -316,6 +319,7 @@ SOURCES += \ src/widgets/settingspages/IgnoresPage.cpp \ src/widgets/settingspages/KeyboardSettingsPage.cpp \ src/widgets/settingspages/ModerationPage.cpp \ + src/widgets/settingspages/NicknamesPage.cpp \ src/widgets/settingspages/NotificationPage.cpp \ src/widgets/settingspages/SettingsPage.cpp \ src/widgets/splits/ClosedSplits.cpp \ @@ -392,6 +396,8 @@ HEADERS += \ src/controllers/ignores/IgnorePhrase.hpp \ src/controllers/moderationactions/ModerationAction.hpp \ src/controllers/moderationactions/ModerationActionModel.hpp \ + src/controllers/nicknames/Nickname.hpp \ + src/controllers/nicknames/NicknamesModel.hpp \ src/controllers/notifications/NotificationController.hpp \ src/controllers/notifications/NotificationModel.hpp \ src/controllers/pings/MutedChannelModel.hpp \ @@ -505,6 +511,7 @@ HEADERS += \ src/util/rangealgorithm.hpp \ src/util/RapidjsonHelpers.hpp \ src/util/RapidJsonSerializeQString.hpp \ + src/util/RatelimitBucket.hpp \ src/util/RemoveScrollAreaBackground.hpp \ src/util/SampleCheerMessages.hpp \ src/util/SampleLinks.hpp \ @@ -580,6 +587,7 @@ HEADERS += \ src/widgets/settingspages/IgnoresPage.hpp \ src/widgets/settingspages/KeyboardSettingsPage.hpp \ src/widgets/settingspages/ModerationPage.hpp \ + src/widgets/settingspages/NicknamesPage.hpp \ src/widgets/settingspages/NotificationPage.hpp \ src/widgets/settingspages/SettingsPage.hpp \ src/widgets/splits/ClosedSplits.hpp \ diff --git a/conanfile.txt b/conanfile.txt index 57d42da73..e4f7eb6ba 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,6 +1,6 @@ [requires] -openssl/1.1.1d -boost/1.75.0 +openssl/1.1.1k +boost/1.76.0 [generators] qmake diff --git a/docs/test-and-benchmark.md b/docs/test-and-benchmark.md new file mode 100644 index 000000000..e881db6be --- /dev/null +++ b/docs/test-and-benchmark.md @@ -0,0 +1,88 @@ +# Test and Benchmark + +Chatterino includes a set of unit tests and benchmarks. These can be built using cmake by adding the `-DBUILD_TESTS=On` and `-DBUILD_BENCHMARKS=On` flags respectively. + +## Adding your own test + +1. Create a new file for the file you're adding tests for. If you're creating tests for `src/providers/emoji/Emojis.cpp`, create `tests/src/Emojis.cpp`. +2. Add the newly created file to `tests/CMakeLists.txt` in the `test_SOURCES` variable (see the comment near it) + +See `tests/src/Emojis.cpp` for simple tests you can base your tests off of. + +Read up on http://google.github.io/googletest/primer.html to figure out how GoogleTest works. + +## Building and running tests + +```sh +mkdir build-tests +cd build-tests +cmake -DBUILD_TESTS=On .. +make +./bin/chatterino-test +``` + +### Example output + +``` +[==========] Running 26 tests from 8 test suites. +[----------] Global test environment set-up. +[----------] 2 tests from AccessGuardLocker +[ RUN ] AccessGuardLocker.NonConcurrentUsage +[ OK ] AccessGuardLocker.NonConcurrentUsage (0 ms) +[ RUN ] AccessGuardLocker.ConcurrentUsage +[ OK ] AccessGuardLocker.ConcurrentUsage (686 ms) +[----------] 2 tests from AccessGuardLocker (686 ms total) + +[----------] 4 tests from NetworkCommon +[ RUN ] NetworkCommon.parseHeaderList1 +[ OK ] NetworkCommon.parseHeaderList1 (0 ms) +[ RUN ] NetworkCommon.parseHeaderListTrimmed +[ OK ] NetworkCommon.parseHeaderListTrimmed (0 ms) +[ RUN ] NetworkCommon.parseHeaderListColonInValue +... +[ RUN ] TwitchAccount.NotEnoughForMoreThanOneBatch +[ OK ] TwitchAccount.NotEnoughForMoreThanOneBatch (0 ms) +[ RUN ] TwitchAccount.BatchThreeParts +[ OK ] TwitchAccount.BatchThreeParts (0 ms) +[----------] 3 tests from TwitchAccount (2 ms total) + +[----------] Global test environment tear-down +[==========] 26 tests from 8 test suites ran. (10297 ms total) +[ PASSED ] 26 tests. +``` + +## Adding your own benchmark + +1. Create a new file for the file you're adding benchmark for. If you're creating benchmarks for `src/providers/emoji/Emojis.cpp`, create `benchmarks/src/Emojis.cpp`. +2. Add the newly created file to `benchmarks/CMakeLists.txt` in the `benchmark_SOURCES` variable (see the comment near it) + +See `benchmarks/src/Emojis.cpp` for simple benchmark you can base your benchmarks off of. + +## Building and running benchmarks + +```sh +mkdir build-benchmarks +cd build-benchmarks +cmake -DBUILD_BENCHMARKS=On .. +make +./bin/chatterino-benchmark +``` + +### Example output + +``` +2021-07-18T13:12:11+02:00 +Running ./bin/chatterino-benchmark +Run on (12 X 4000 MHz CPU s) +CPU Caches: + L1 Data 32 KiB (x6) + L1 Instruction 32 KiB (x6) + L2 Unified 256 KiB (x6) + L3 Unified 15360 KiB (x1) +Load Average: 2.86, 3.08, 3.51 +***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead. +-------------------------------------------------------------- +Benchmark Time CPU Iterations +-------------------------------------------------------------- +BM_ShortcodeParsing 2394 ns 2389 ns 278933 +``` diff --git a/lib/libcommuni b/lib/libcommuni index ef8daa149..95f05478d 160000 --- a/lib/libcommuni +++ b/lib/libcommuni @@ -1 +1 @@ -Subproject commit ef8daa14946b8e19f536200e28db2b25e8311ba5 +Subproject commit 95f05478de1623767282d8019ea8f3a4b1178b35 diff --git a/resources/avatars/alazymeme.png b/resources/avatars/alazymeme.png new file mode 100644 index 000000000..66ac412bb Binary files /dev/null and b/resources/avatars/alazymeme.png differ diff --git a/resources/avatars/xheaveny.png b/resources/avatars/xheaveny.png new file mode 100644 index 000000000..0977b6402 Binary files /dev/null and b/resources/avatars/xheaveny.png differ diff --git a/resources/buttons/copyDark.png b/resources/buttons/copyDark.png index 2a663bfd6..a0b633eec 100644 Binary files a/resources/buttons/copyDark.png and b/resources/buttons/copyDark.png differ diff --git a/resources/buttons/copyDark.svg b/resources/buttons/copyDark.svg index ed30f70a7..5fddace4e 100644 --- a/resources/buttons/copyDark.svg +++ b/resources/buttons/copyDark.svg @@ -9,5 +9,5 @@ height="368.64pt" viewBox="0 0 368.64 368.64"> - + diff --git a/resources/buttons/copyLight.png b/resources/buttons/copyLight.png index a0b633eec..2a663bfd6 100644 Binary files a/resources/buttons/copyLight.png and b/resources/buttons/copyLight.png differ diff --git a/resources/buttons/copyLight.svg b/resources/buttons/copyLight.svg index 5fddace4e..ed30f70a7 100644 --- a/resources/buttons/copyLight.svg +++ b/resources/buttons/copyLight.svg @@ -9,5 +9,5 @@ height="368.64pt" viewBox="0 0 368.64 368.64"> - + diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index a0802b163..5c8314f08 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -32,6 +32,6 @@ chatterino - + diff --git a/resources/contributors.txt b/resources/contributors.txt index 062ad0ed8..13e67ea9b 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -43,6 +43,8 @@ matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg | Contributor Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor Talen | https://github.com/talneoran | | Contributor SLCH | https://github.com/SLCH | :/avatars/slch.png | Contributor +ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png | Contributor +xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png | Contributor # If you are a contributor add yourself above this line diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index 15716d86e..6ea9d2e51 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -1,6 +1,7 @@ auth.html + avatars/alazymeme.png avatars/fourtf.png avatars/kararty.png avatars/matthewde.jpg @@ -8,6 +9,7 @@ avatars/pajlada.png avatars/revolter.jpg avatars/slch.png + avatars/xheaveny.png avatars/zneix.png buttons/addSplit.png buttons/addSplitDark.png diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8190e576e..9d0fb96d8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,6 +88,8 @@ set(SOURCE_FILES controllers/highlights/UserHighlightModel.cpp controllers/highlights/UserHighlightModel.hpp + controllers/ignores/IgnoreController.cpp + controllers/ignores/IgnoreController.hpp controllers/ignores/IgnoreModel.cpp controllers/ignores/IgnoreModel.hpp @@ -96,6 +98,10 @@ set(SOURCE_FILES controllers/moderationactions/ModerationActionModel.cpp controllers/moderationactions/ModerationActionModel.hpp + controllers/nicknames/NicknamesModel.cpp + controllers/nicknames/NicknamesModel.hpp + controllers/nicknames/Nickname.hpp + controllers/notifications/NotificationController.cpp controllers/notifications/NotificationController.hpp controllers/notifications/NotificationModel.cpp @@ -286,6 +292,8 @@ set(SOURCE_FILES util/NuulsUploader.hpp util/RapidjsonHelpers.cpp util/RapidjsonHelpers.hpp + util/RatelimitBucket.cpp + util/RatelimitBucket.hpp util/SplitCommand.cpp util/SplitCommand.hpp util/StreamLink.cpp @@ -430,6 +438,8 @@ set(SOURCE_FILES widgets/settingspages/KeyboardSettingsPage.hpp widgets/settingspages/ModerationPage.cpp widgets/settingspages/ModerationPage.hpp + widgets/settingspages/NicknamesPage.cpp + widgets/settingspages/NicknamesPage.hpp widgets/settingspages/NotificationPage.cpp widgets/settingspages/NotificationPage.hpp widgets/settingspages/SettingsPage.cpp @@ -478,17 +488,17 @@ add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES}) target_link_libraries(${LIBRARY_PROJECT} PUBLIC - Qt5::Core - Qt5::Widgets - Qt5::Gui - Qt5::Network - Qt5::Multimedia - Qt5::Svg - Qt5::Concurrent - Qt5::HttpServer + Qt${MAJOR_QT_VERSION}::Core + Qt${MAJOR_QT_VERSION}::Widgets + Qt${MAJOR_QT_VERSION}::Gui + Qt${MAJOR_QT_VERSION}::Network + Qt${MAJOR_QT_VERSION}::Multimedia + Qt${MAJOR_QT_VERSION}::Svg + Qt${MAJOR_QT_VERSION}::Concurrent + Qt${MAJOR_QT_VERSION}::HttpServer LibCommuni::LibCommuni - qt5keychain + qt${MAJOR_QT_VERSION}keychain Pajlada::Serialize Pajlada::Settings Pajlada::Signals @@ -517,8 +527,8 @@ if (BUILD_APP) ) if (MSVC) - get_target_property(Qt5_Core_Location Qt5::Core LOCATION) - get_filename_component(QT_BIN_DIR ${Qt5_Core_Location} DIRECTORY) + get_target_property(Qt_Core_Location Qt${MAJOR_QT_VERSION}::Core LOCATION) + get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY) set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $ --release --no-compiler-runtime --no-translations --no-opengl-sw) install(TARGETS ${EXECUTABLE_PROJECT} @@ -575,7 +585,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\" CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\" ) -if (USE_SYSTEM_QT5KEYCHAIN) +if (USE_SYSTEM_QTKEYCHAIN) target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CMAKE_BUILD ) diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp index d4b48b970..c1bc17322 100644 --- a/src/autogenerated/ResourcesAutogen.cpp +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -4,11 +4,13 @@ namespace chatterino { Resources2::Resources2() { + this->avatars.alazymeme = QPixmap(":/avatars/alazymeme.png"); this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); this->avatars.kararty = QPixmap(":/avatars/kararty.png"); this->avatars.mm2pl = QPixmap(":/avatars/mm2pl.png"); this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); this->avatars.slch = QPixmap(":/avatars/slch.png"); + this->avatars.xheaveny = QPixmap(":/avatars/xheaveny.png"); this->avatars.zneix = QPixmap(":/avatars/zneix.png"); this->buttons.addSplit = QPixmap(":/buttons/addSplit.png"); this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png"); diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp index 7277cc681..c8b102fbb 100644 --- a/src/autogenerated/ResourcesAutogen.hpp +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -9,11 +9,13 @@ public: Resources2(); struct { + QPixmap alazymeme; QPixmap fourtf; QPixmap kararty; QPixmap mm2pl; QPixmap pajlada; QPixmap slch; + QPixmap xheaveny; QPixmap zneix; } avatars; struct { diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 29c88e21a..1a858c0ea 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -3,7 +3,7 @@ #include #include -#define CHATTERINO_VERSION "2.3.3" +#define CHATTERINO_VERSION "2.3.4" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index fcca4a2f9..8e977af9b 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -70,6 +70,32 @@ static const QStringList twitchDefaultCommands{ static const QStringList whisperCommands{"/w", ".w"}; +// stripUserName removes any @ prefix or , suffix to make it more suitable for command use +void stripUserName(QString &userName) +{ + if (userName.startsWith('@')) + { + userName.remove(0, 1); + } + if (userName.endsWith(',')) + { + userName.chop(1); + } +} + +// stripChannelName removes any @ prefix or , suffix to make it more suitable for command use +void stripChannelName(QString &channelName) +{ + if (channelName.startsWith('@') || channelName.startsWith('#')) + { + channelName.remove(0, 1); + } + if (channelName.endsWith(',')) + { + channelName.chop(1); + } +} + void sendWhisperMessage(const QString &text) { // (hemirt) pajlada: "we should not be sending whispers through jtv, but @@ -373,6 +399,22 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); + this->registerCommand("/follow", [](const auto &words, auto channel) { + channel->addMessage(makeSystemMessage( + "Twitch has removed the ability to follow users through " + "third-party applications. For more information, see " + "https://github.com/Chatterino/chatterino2/issues/3076")); + return ""; + }); + + this->registerCommand("/unfollow", [](const auto &words, auto channel) { + channel->addMessage(makeSystemMessage( + "Twitch has removed the ability to unfollow users through " + "third-party applications. For more information, see " + "https://github.com/Chatterino/chatterino2/issues/3076")); + return ""; + }); + /// Supported commands this->registerCommand( @@ -407,90 +449,6 @@ void CommandController::initialize(Settings &, Paths &paths) this->registerCommand("/unblock", unblockLambda); - this->registerCommand("/follow", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /follow [user]")); - return ""; - } - - auto currentUser = getApp()->accounts->twitch.getCurrent(); - - if (currentUser->isAnon()) - { - channel->addMessage( - makeSystemMessage("You must be logged in to follow someone!")); - return ""; - } - - auto target = words.at(1); - - getHelix()->getUserByName( - target, - [currentUser, channel, target](const auto &targetUser) { - getHelix()->followUser( - currentUser->getUserId(), targetUser.id, - [channel, target]() { - channel->addMessage(makeSystemMessage( - "You successfully followed " + target)); - }, - [channel, target]() { - channel->addMessage(makeSystemMessage( - QString("User %1 could not be followed, an unknown " - "error occurred!") - .arg(target))); - }); - }, - [channel, target] { - channel->addMessage( - makeSystemMessage(QString("User %1 could not be followed, " - "no user with that name found!") - .arg(target))); - }); - - return ""; - }); - - this->registerCommand("/unfollow", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /unfollow [user]")); - return ""; - } - - auto currentUser = getApp()->accounts->twitch.getCurrent(); - - if (currentUser->isAnon()) - { - channel->addMessage(makeSystemMessage( - "You must be logged in to unfollow someone!")); - return ""; - } - - auto target = words.at(1); - - getHelix()->getUserByName( - target, - [currentUser, channel, target](const auto &targetUser) { - getHelix()->unfollowUser( - currentUser->getUserId(), targetUser.id, - [channel, target]() { - channel->addMessage(makeSystemMessage( - "You successfully unfollowed " + target)); - }, - [channel, target]() { - channel->addMessage(makeSystemMessage( - "An error occurred while unfollowing " + target)); - }); - }, - [channel, target] { - channel->addMessage(makeSystemMessage( - QString("User %1 could not be followed!").arg(target))); - }); - - return ""; - }); - this->registerCommand("/user", [](const auto &words, auto channel) { if (words.size() < 2) { @@ -498,16 +456,17 @@ void CommandController::initialize(Settings &, Paths &paths) makeSystemMessage("Usage /user [user] (channel)")); return ""; } + QString userName = words[1]; + stripUserName(userName); + QString channelName = channel->getName(); + if (words.size() > 2) { channelName = words[2]; - if (channelName[0] == '#') - { - channelName.remove(0, 1); - } + stripChannelName(channelName); } - openTwitchUsercard(channelName, words[1]); + openTwitchUsercard(channelName, userName); return ""; }); @@ -519,10 +478,12 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; } + QString userName = words[1]; + stripUserName(userName); auto *userPopup = new UserInfoPopup( getSettings()->autoCloseUserPopup, static_cast(&(getApp()->windows->getMainWindow()))); - userPopup->setData(words[1], channel); + userPopup->setData(userName, channel); userPopup->move(QCursor::pos()); userPopup->show(); return ""; diff --git a/src/controllers/filters/FilterRecord.hpp b/src/controllers/filters/FilterRecord.hpp index 8d699f828..5f0eb750c 100644 --- a/src/controllers/filters/FilterRecord.hpp +++ b/src/controllers/filters/FilterRecord.hpp @@ -60,11 +60,6 @@ public: return this->parser_->valid(); } - bool filter(const MessagePtr &message) const - { - return this->parser_->execute(message); - } - bool filter(const filterparser::ContextMap &context) const { return this->parser_->execute(context); diff --git a/src/controllers/filters/FilterSet.hpp b/src/controllers/filters/FilterSet.hpp index 52a953e54..687f79964 100644 --- a/src/controllers/filters/FilterSet.hpp +++ b/src/controllers/filters/FilterSet.hpp @@ -36,12 +36,13 @@ public: this->listener_.disconnect(); } - bool filter(const MessagePtr &m) const + bool filter(const MessagePtr &m, ChannelPtr channel) const { if (this->filters_.size() == 0) return true; - filterparser::ContextMap context = filterparser::buildContextMap(m); + filterparser::ContextMap context = + filterparser::buildContextMap(m, channel.get()); for (const auto &f : this->filters_.values()) { if (!f->valid() || !f->filter(context)) diff --git a/src/controllers/filters/parser/FilterParser.cpp b/src/controllers/filters/parser/FilterParser.cpp index d7cd18822..c4dca050a 100644 --- a/src/controllers/filters/parser/FilterParser.cpp +++ b/src/controllers/filters/parser/FilterParser.cpp @@ -1,12 +1,13 @@ #include "FilterParser.hpp" #include "Application.hpp" +#include "common/Channel.hpp" #include "controllers/filters/parser/Types.hpp" #include "providers/twitch/TwitchIrcServer.hpp" namespace filterparser { -ContextMap buildContextMap(const MessagePtr &m) +ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) { auto watchingChannel = chatterino::getApp()->twitch.server->watchingChannel.get(); @@ -61,8 +62,7 @@ ContextMap buildContextMap(const MessagePtr &m) subLength = m->badgeInfos.at(subBadge).toInt(); } } - - return { + ContextMap vars = { {"author.badges", std::move(badges)}, {"author.color", m->usernameColor}, {"author.name", m->displayName}, @@ -82,6 +82,19 @@ ContextMap buildContextMap(const MessagePtr &m) {"message.content", m->messageText}, {"message.length", m->messageText.length()}, }; + { + using namespace chatterino; + auto *tc = dynamic_cast(channel); + if (channel && !channel->isEmpty() && tc) + { + vars["channel.live"] = tc->isLive(); + } + else + { + vars["channel.live"] = false; + } + } + return vars; } FilterParser::FilterParser(const QString &text) @@ -91,12 +104,6 @@ FilterParser::FilterParser(const QString &text) { } -bool FilterParser::execute(const MessagePtr &message) const -{ - auto context = buildContextMap(message); - return this->execute(context); -} - bool FilterParser::execute(const ContextMap &context) const { return this->builtExpression_->execute(context).toBool(); diff --git a/src/controllers/filters/parser/FilterParser.hpp b/src/controllers/filters/parser/FilterParser.hpp index 0c144fae5..70037993e 100644 --- a/src/controllers/filters/parser/FilterParser.hpp +++ b/src/controllers/filters/parser/FilterParser.hpp @@ -3,15 +3,20 @@ #include "controllers/filters/parser/Tokenizer.hpp" #include "controllers/filters/parser/Types.hpp" +namespace chatterino { + +class Channel; + +} // namespace chatterino + namespace filterparser { -ContextMap buildContextMap(const MessagePtr &m); +ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); class FilterParser { public: FilterParser(const QString &text); - bool execute(const MessagePtr &message) const; bool execute(const ContextMap &context) const; bool valid() const; diff --git a/src/controllers/filters/parser/Tokenizer.hpp b/src/controllers/filters/parser/Tokenizer.hpp index 78ff27064..8f9b5824b 100644 --- a/src/controllers/filters/parser/Tokenizer.hpp +++ b/src/controllers/filters/parser/Tokenizer.hpp @@ -17,6 +17,7 @@ static const QMap validIdentifiersMap = { {"author.sub_length", "author sub length"}, {"channel.name", "channel name"}, {"channel.watching", "/watching channel?"}, + {"channel.live", "Channel live?"}, {"flags.highlighted", "highlighted?"}, {"flags.points_redeemed", "redeemed points?"}, {"flags.sub_message", "sub/resub message?"}, diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp new file mode 100644 index 000000000..e36feead0 --- /dev/null +++ b/src/controllers/ignores/IgnoreController.cpp @@ -0,0 +1,63 @@ +#include "controllers/ignores/IgnoreController.hpp" + +#include "common/QLogging.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +bool isIgnoredMessage(IgnoredMessageParameters &¶ms) +{ + if (!params.message.isEmpty()) + { + // TODO(pajlada): Do we need to check if the phrase is valid first? + auto phrases = getCSettings().ignoredMessages.readOnly(); + for (const auto &phrase : *phrases) + { + if (phrase.isBlock() && phrase.isMatch(params.message)) + { + qCDebug(chatterinoMessage) + << "Blocking message because it contains ignored phrase" + << phrase.getPattern(); + return true; + } + } + } + + if (!params.twitchUserID.isEmpty() && + getSettings()->enableTwitchBlockedUsers) + { + auto sourceUserID = params.twitchUserID; + + auto blocks = + getApp()->accounts->twitch.getCurrent()->accessBlockedUserIds(); + + if (auto it = blocks->find(sourceUserID); it != blocks->end()) + { + switch (static_cast( + getSettings()->showBlockedUsersMessages.getValue())) + { + case ShowIgnoredUsersMessages::IfModerator: + if (params.isMod || params.isBroadcaster) + { + return false; + } + break; + case ShowIgnoredUsersMessages::IfBroadcaster: + if (params.isBroadcaster) + { + return false; + } + break; + case ShowIgnoredUsersMessages::Never: + break; + } + + return true; + } + } + + return false; +} + +} // namespace chatterino diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index fed12f12c..4c2048621 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -1,7 +1,19 @@ #pragma once +#include + namespace chatterino { enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster }; +struct IgnoredMessageParameters { + QString message; + + QString twitchUserID; + bool isMod; + bool isBroadcaster; +}; + +bool isIgnoredMessage(IgnoredMessageParameters &¶ms); + } // namespace chatterino diff --git a/src/controllers/nicknames/Nickname.hpp b/src/controllers/nicknames/Nickname.hpp new file mode 100644 index 000000000..67e7d3f40 --- /dev/null +++ b/src/controllers/nicknames/Nickname.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "controllers/accounts/AccountController.hpp" + +#include "util/RapidJsonSerializeQString.hpp" +#include "util/RapidjsonHelpers.hpp" + +#include +#include + +#include + +namespace chatterino { + +class Nickname +{ +public: + Nickname(const QString &name, const QString &replace) + : name_(name) + , replace_(replace) + { + } + + const QString &name() const + { + return this->name_; + } + const QString &replace() const + { + return this->replace_; + } + +private: + QString name_; + QString replace_; +}; + +} // namespace chatterino + +namespace pajlada { + +template <> +struct Serialize { + static rapidjson::Value get(const chatterino::Nickname &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); + + chatterino::rj::set(ret, "name", value.name(), a); + chatterino::rj::set(ret, "replace", value.replace(), a); + + return ret; + } +}; + +template <> +struct Deserialize { + static chatterino::Nickname get(const rapidjson::Value &value, + bool *error = nullptr) + { + if (!value.IsObject()) + { + PAJLADA_REPORT_ERROR(error) + return chatterino::Nickname(QString(), QString()); + } + + QString _name; + QString _replace; + + chatterino::rj::getSafe(value, "name", _name); + chatterino::rj::getSafe(value, "replace", _replace); + + return chatterino::Nickname(_name, _replace); + } +}; + +} // namespace pajlada diff --git a/src/controllers/nicknames/NicknamesModel.cpp b/src/controllers/nicknames/NicknamesModel.cpp new file mode 100644 index 000000000..703c6661b --- /dev/null +++ b/src/controllers/nicknames/NicknamesModel.cpp @@ -0,0 +1,31 @@ +#include "NicknamesModel.hpp" + +#include "Application.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "singletons/Settings.hpp" +#include "util/StandardItemHelper.hpp" + +namespace chatterino { + +NicknamesModel::NicknamesModel(QObject *parent) + : SignalVectorModel(2, parent) +{ +} + +// turn a vector item into a model row +Nickname NicknamesModel::getItemFromRow(std::vector &row, + const Nickname &original) +{ + return Nickname{row[0]->data(Qt::DisplayRole).toString(), + row[1]->data(Qt::DisplayRole).toString()}; +} + +// turns a row in the model into a vector item +void NicknamesModel::getRowFromItem(const Nickname &item, + std::vector &row) +{ + setStringItem(row[0], item.name()); + setStringItem(row[1], item.replace()); +} + +} // namespace chatterino diff --git a/src/controllers/nicknames/NicknamesModel.hpp b/src/controllers/nicknames/NicknamesModel.hpp new file mode 100644 index 000000000..f7947a7fa --- /dev/null +++ b/src/controllers/nicknames/NicknamesModel.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "common/SignalVectorModel.hpp" +#include "controllers/nicknames/Nickname.hpp" + +namespace chatterino { + +class NicknamesModel : public SignalVectorModel +{ +public: + explicit NicknamesModel(QObject *parent); + +protected: + // turn a vector item into a model row + virtual Nickname getItemFromRow(std::vector &row, + const Nickname &original) override; + + // turns a row in the model into a vector item + virtual void getRowFromItem(const Nickname &item, + std::vector &row) override; +}; + +} // namespace chatterino diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index 1235af3c6..cdc8e59f0 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" @@ -104,20 +105,9 @@ void SharedMessageBuilder::parse() bool SharedMessageBuilder::isIgnored() const { - // TODO(pajlada): Do we need to check if the phrase is valid first? - auto phrases = getCSettings().ignoredMessages.readOnly(); - for (const auto &phrase : *phrases) - { - if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) - { - qCDebug(chatterinoMessage) - << "Blocking message because it contains ignored phrase" - << phrase.getPattern(); - return true; - } - } - - return false; + return isIgnoredMessage({ + /*.message = */ this->originalMessage_, + }); } void SharedMessageBuilder::parseUsernameColor() diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index be9fc4f88..0ccebdec9 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -15,6 +15,10 @@ const int RECONNECT_BASE_INTERVAL = 2000; // 60 falloff counter means it will try to reconnect at most every 60*2 seconds const int MAX_FALLOFF_COUNTER = 60; +// Ratelimits for joinBucket_ +const int JOIN_RATELIMIT_BUDGET = 18; +const int JOIN_RATELIMIT_COOLDOWN = 10500; + AbstractIrcServer::AbstractIrcServer() { // Initialize the connections @@ -23,6 +27,17 @@ AbstractIrcServer::AbstractIrcServer() this->writeConnection_->moveToThread( QCoreApplication::instance()->thread()); + // Apply a leaky bucket rate limiting to JOIN messages + auto actuallyJoin = [&](QString message) { + if (!this->channels.contains(message)) + { + return; + } + this->readConnection_->sendRaw("JOIN #" + message); + }; + this->joinBucket_.reset(new RatelimitBucket( + JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this)); + QObject::connect(this->writeConnection_.get(), &Communi::IrcConnection::messageReceived, this, [this](auto msg) { @@ -214,11 +229,6 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) { this->readConnection_->sendRaw("PART #" + channelName); } - - if (this->writeConnection_ && this->hasSeparateWriteConnection()) - { - this->writeConnection_->sendRaw("PART #" + channelName); - } })); // join irc channel @@ -229,15 +239,7 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) { if (this->readConnection_->isConnected()) { - this->readConnection_->sendRaw("JOIN #" + channelName); - } - } - - if (this->writeConnection_ && this->hasSeparateWriteConnection()) - { - if (this->readConnection_->isConnected()) - { - this->writeConnection_->sendRaw("JOIN #" + channelName); + this->joinBucket_->send(channelName); } } } @@ -297,7 +299,7 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection) { if (auto channel = weak.lock()) { - connection->sendRaw("JOIN #" + channel->getName()); + this->joinBucket_->send(channel->getName()); } } diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 7cfc38f5e..40da3ba20 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -8,6 +8,7 @@ #include "common/Common.hpp" #include "providers/irc/IrcConnection2.hpp" +#include "util/RatelimitBucket.hpp" namespace chatterino { @@ -88,6 +89,10 @@ private: QObjectPtr writeConnection_ = nullptr; QObjectPtr readConnection_ = nullptr; + // Our rate limiting bucket for the Twitch join rate limits + // https://dev.twitch.tv/docs/irc/guide#rate-limits + QObjectPtr joinBucket_; + QTimer reconnectTimer_; int falloffCounter_ = 1; diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 1dfcd02e9..af131bbce 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -3,6 +3,7 @@ #include "providers/twitch/PubsubActions.hpp" #include "providers/twitch/PubsubHelpers.hpp" #include "singletons/Settings.hpp" +#include "util/DebugCount.hpp" #include "util/Helpers.hpp" #include "util/RapidjsonHelpers.hpp" @@ -23,7 +24,8 @@ namespace chatterino { static const char *pingPayload = "{\"type\":\"PING\"}"; -static std::map sentMessages; +static std::map sentListens; +static std::map sentUnlistens; namespace detail { @@ -59,8 +61,9 @@ namespace detail { // This PubSubClient is already at its peak listens return false; } - this->numListens_ += numRequestedListens; + DebugCount::increase("PubSub topic pending listens", + numRequestedListens); for (const auto &topic : message["data"]["topics"].GetArray()) { @@ -68,12 +71,11 @@ namespace detail { Listener{topic.GetString(), false, false, false}); } - auto uuid = generateUuid(); - - rj::set(message, "nonce", uuid); + auto nonce = generateUuid(); + rj::set(message, "nonce", nonce); QString payload = rj::stringify(message); - sentMessages[uuid] = payload; + sentListens[nonce] = RequestMessage{payload, numRequestedListens}; this->send(payload.toUtf8()); @@ -103,14 +105,19 @@ namespace detail { return; } + int numRequestedUnlistens = topics.size(); + + this->numListens_ -= numRequestedUnlistens; + DebugCount::increase("PubSub topic pending unlistens", + numRequestedUnlistens); + auto message = createUnlistenMessage(topics); - auto uuid = generateUuid(); - - rj::set(message, "nonce", generateUuid()); + auto nonce = generateUuid(); + rj::set(message, "nonce", nonce); QString payload = rj::stringify(message); - sentMessages[uuid] = payload; + sentUnlistens[nonce] = RequestMessage{payload, numRequestedUnlistens}; this->send(payload.toUtf8()); } @@ -865,6 +872,13 @@ PubSub::PubSub() void PubSub::addClient() { + if (this->addingClient) + { + return; + } + + this->addingClient = true; + websocketpp::lib::error_code ec; auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, ec); @@ -998,6 +1012,8 @@ void PubSub::listen(rapidjson::Document &&msg) this->requests.emplace_back( std::make_unique(std::move(msg))); + + DebugCount::increase("PubSub topic backlog"); } bool PubSub::tryListen(rapidjson::Document &msg) @@ -1066,7 +1082,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, if (type == "RESPONSE") { - this->handleListenResponse(msg); + this->handleResponse(msg); } else if (type == "MESSAGE") { @@ -1107,6 +1123,9 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, void PubSub::onConnectionOpen(WebsocketHandle hdl) { + DebugCount::increase("PubSub connections"); + this->addingClient = false; + auto client = std::make_shared(this->websocketClient, hdl); @@ -1123,6 +1142,7 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl) const auto &request = *it; if (client->listen(*request)) { + DebugCount::decrease("PubSub topic backlog"); it = this->requests.erase(it); } else @@ -1130,10 +1150,16 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl) ++it; } } + + if (!this->requests.empty()) + { + this->addClient(); + } } void PubSub::onConnectionClose(WebsocketHandle hdl) { + DebugCount::decrease("PubSub connections"); auto clientIt = this->clients.find(hdl); // If this assert goes off, there's something wrong with the connection @@ -1169,26 +1195,63 @@ PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl) return ctx; } -void PubSub::handleListenResponse(const rapidjson::Document &msg) +void PubSub::handleResponse(const rapidjson::Document &msg) { QString error; - if (rj::getSafe(msg, "error", error)) - { - QString nonce; - rj::getSafe(msg, "nonce", nonce); - - if (error.isEmpty()) - { - qCDebug(chatterinoPubsub) - << "Successfully listened to nonce" << nonce; - // Nothing went wrong - return; - } - - qCDebug(chatterinoPubsub) - << "PubSub error:" << error << "on nonce" << nonce; + if (!rj::getSafe(msg, "error", error)) return; + + QString nonce; + rj::getSafe(msg, "nonce", nonce); + + const bool failed = !error.isEmpty(); + + if (failed) + { + qCDebug(chatterinoPubsub) + << QString("Error %1 on nonce %2").arg(error, nonce); + } + + if (auto it = sentListens.find(nonce); it != sentListens.end()) + { + this->handleListenResponse(it->second, failed); + return; + } + + if (auto it = sentUnlistens.find(nonce); it != sentUnlistens.end()) + { + this->handleUnlistenResponse(it->second, failed); + return; + } + + qCDebug(chatterinoPubsub) + << "Response on unused" << nonce << "client/topic listener mismatch?"; +} + +void PubSub::handleListenResponse(const RequestMessage &msg, bool failed) +{ + DebugCount::decrease("PubSub topic pending listens", msg.topicCount); + if (failed) + { + DebugCount::increase("PubSub topic failed listens", msg.topicCount); + } + else + { + DebugCount::increase("PubSub topic listening", msg.topicCount); + } +} + +void PubSub::handleUnlistenResponse(const RequestMessage &msg, bool failed) +{ + DebugCount::decrease("PubSub topic pending unlistens", msg.topicCount); + if (failed) + { + DebugCount::increase("PubSub topic failed unlistens", msg.topicCount); + } + else + { + DebugCount::decrease("PubSub topic listening", msg.topicCount); } } diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index 1e5a01d1f..06c8d50b8 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -47,6 +47,11 @@ using WebsocketErrorCode = websocketpp::lib::error_code; #define MAX_PUBSUB_LISTENS 50 #define MAX_PUBSUB_CONNECTIONS 10 +struct RequestMessage { + QString payload; + int topicCount; +}; + namespace detail { struct Listener { @@ -172,6 +177,7 @@ private: bool isListeningToTopic(const QString &topic); void addClient(); + std::atomic addingClient{false}; State state = State::Connected; @@ -192,7 +198,9 @@ private: void onConnectionClose(websocketpp::connection_hdl hdl); WebsocketContextPtr onTLSInit(websocketpp::connection_hdl hdl); - void handleListenResponse(const rapidjson::Document &msg); + void handleResponse(const rapidjson::Document &msg); + void handleListenResponse(const RequestMessage &msg, bool failed); + void handleUnlistenResponse(const RequestMessage &msg, bool failed); void handleMessageResponse(const rapidjson::Value &data); void runThread(); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 416d5ba61..786a0cbe3 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -186,23 +186,6 @@ void TwitchAccount::unblockUser(QString userId, std::function onSuccess, std::move(onFailure)); } -void TwitchAccount::checkFollow(const QString targetUserID, - std::function onFinished) -{ - const auto onResponse = [onFinished](bool following, const auto &record) { - if (!following) - { - onFinished(FollowResult_NotFollowing); - return; - } - - onFinished(FollowResult_Following); - }; - - getHelix()->getUserFollow(this->getUserId(), targetUserID, onResponse, - [] {}); -} - SharedAccessGuard> TwitchAccount::accessBlocks() const { @@ -215,7 +198,7 @@ SharedAccessGuard> TwitchAccount::accessBlockedUserIds() return this->ignoresUserIds_.accessConst(); } -void TwitchAccount::loadEmotes() +void TwitchAccount::loadEmotes(std::weak_ptr weakChannel) { qCDebug(chatterinoTwitch) << "Loading Twitch emotes for user" << this->getUserName(); @@ -237,9 +220,14 @@ void TwitchAccount::loadEmotes() // TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution // For now, this is necessary as Kraken's equivalent doesn't return all emotes // See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900 - this->loadUserstateEmotes([=] { + this->loadUserstateEmotes([this, weakChannel] { // Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData. this->loadKrakenEmotes(); + if (auto channel = weakChannel.lock(); channel != nullptr) + { + channel->addMessage( + makeSystemMessage("Twitch subscriber emotes reloaded.")); + } }); } diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index af56afaf1..034912a24 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -108,13 +108,10 @@ public: void unblockUser(QString userId, std::function onSuccess, std::function onFailure); - void checkFollow(const QString targetUserID, - std::function onFinished); - SharedAccessGuard> accessBlockedUserIds() const; SharedAccessGuard> accessBlocks() const; - void loadEmotes(); + void loadEmotes(std::weak_ptr weakChannel = {}); // loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key // this function makes sure not to load emote sets that have already been loaded void loadUserstateEmotes(std::function callback); diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 4571cca22..94c458a55 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -286,7 +286,8 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) if (!reward.isUserInputRequired) { MessageBuilder builder; - TwitchMessageBuilder::appendChannelPointRewardMessage(reward, &builder); + TwitchMessageBuilder::appendChannelPointRewardMessage( + reward, &builder, this->isMod(), this->isBroadcaster()); this->addMessage(builder.release()); return; } @@ -375,7 +376,17 @@ void TwitchChannel::sendMessage(const QString &message) { if (parsedMessage == this->lastSentMessage_) { - parsedMessage.append(MAGIC_MESSAGE_SUFFIX); + auto spaceIndex = parsedMessage.indexOf(' '); + if (spaceIndex == -1) + { + // no spaces found, fall back to old magic character + parsedMessage.append(MAGIC_MESSAGE_SUFFIX); + } + else + { + // replace the space we found in spaceIndex with two spaces + parsedMessage.replace(spaceIndex, 1, " "); + } } } } diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 38593a03a..9a84bcabf 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -57,7 +57,7 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, Image::fromUrl(getEmoteLink(id, "3.0"), 0.25), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, - Url{QString("https://twitchemotes.com/emotes/%1").arg(id.string)}}); + }); } return shared; diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 3aede1400..8a0aa1642 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -197,34 +197,13 @@ void TwitchIrcServer::writeConnectionMessageReceived( // Below commands enabled through the twitch.tv/commands CAP REQ if (command == "USERSTATE") { - // Received USERSTATE upon PRIVMSGing + // Received USERSTATE upon sending PRIVMSG messages handler.handleUserStateMessage(message); } else if (command == "NOTICE") { - static std::unordered_set readConnectionOnlyIDs{ - "host_on", - "host_off", - "host_target_went_offline", - "emote_only_on", - "emote_only_off", - "slow_on", - "slow_off", - "subs_on", - "subs_off", - "r9k_on", - "r9k_off", - - // Display for user who times someone out. This implies you're a - // moderator, at which point you will be connected to PubSub and receive - // a better message from there. - "timeout_success", - "ban_success", - - // Channel suspended notices - "msg_channel_suspended", - }; - + // List of expected NOTICE messages on write connection + // https://git.kotmisia.pl/Mm2PL/docs/src/branch/master/irc_msg_ids.md#command-results handler.handleNoticeMessage( static_cast(message)); } diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index ed862ca4c..97c36f0d7 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -114,44 +114,12 @@ TwitchMessageBuilder::TwitchMessageBuilder( bool TwitchMessageBuilder::isIgnored() const { - if (SharedMessageBuilder::isIgnored()) - { - return true; - } - - auto app = getApp(); - - if (getSettings()->enableTwitchBlockedUsers && - this->tags.contains("user-id")) - { - auto sourceUserID = this->tags.value("user-id").toString(); - - auto blocks = - app->accounts->twitch.getCurrent()->accessBlockedUserIds(); - - if (auto it = blocks->find(sourceUserID); it != blocks->end()) - { - switch (static_cast( - getSettings()->showBlockedUsersMessages.getValue())) - { - case ShowIgnoredUsersMessages::IfModerator: - if (this->channel->isMod() || - this->channel->isBroadcaster()) - return false; - break; - case ShowIgnoredUsersMessages::IfBroadcaster: - if (this->channel->isBroadcaster()) - return false; - break; - case ShowIgnoredUsersMessages::Never: - break; - } - - return true; - } - } - - return false; + return isIgnoredMessage({ + /*.message = */ this->originalMessage_, + /*.twitchUserID = */ this->tags.value("user-id").toString(), + /*.isMod = */ this->channel->isMod(), + /*.isBroadcaster = */ this->channel->isBroadcaster(), + }); } void TwitchMessageBuilder::triggerHighlights() @@ -190,7 +158,9 @@ MessagePtr TwitchMessageBuilder::build() this->args.channelPointRewardId); if (reward) { - this->appendChannelPointRewardMessage(reward.get(), this); + this->appendChannelPointRewardMessage( + reward.get(), this, this->channel->isMod(), + this->channel->isBroadcaster()); } } @@ -690,6 +660,18 @@ void TwitchMessageBuilder::appendUsername() break; } + auto nicknames = getCSettings().nicknames.readOnly(); + auto loginLower = this->message().loginName.toLower(); + + for (const auto &nickname : *nicknames) + { + if (nickname.name().toLower() == loginLower) + { + usernameText = nickname.replace(); + break; + } + } + if (this->args.isSentWhisper) { // TODO(pajlada): Re-implement @@ -1249,8 +1231,19 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) } void TwitchMessageBuilder::appendChannelPointRewardMessage( - const ChannelPointReward &reward, MessageBuilder *builder) + const ChannelPointReward &reward, MessageBuilder *builder, bool isMod, + bool isBroadcaster) { + if (isIgnoredMessage({ + /*.message = */ "", + /*.twitchUserID = */ reward.user.id, + /*.isMod = */ isMod, + /*.isBroadcaster = */ isBroadcaster, + })) + { + return; + } + builder->emplace(); QString redeemed = "Redeemed"; QStringList textList; diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 3e14412b7..7cf68494a 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -46,7 +46,8 @@ public: MessagePtr build() override; static void appendChannelPointRewardMessage( - const ChannelPointReward &reward, MessageBuilder *builder); + const ChannelPointReward &reward, MessageBuilder *builder, bool isMod, + bool isBroadcaster); // Message in the /live chat for channel going live static void liveMessage(const QString &channelName, diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 2fe734a53..7895995d2 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -142,25 +142,6 @@ void Helix::getUserFollowers( std::move(failureCallback)); } -void Helix::getUserFollow( - QString userId, QString targetId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) -{ - this->fetchUsersFollows( - std::move(userId), std::move(targetId), - [successCallback](const auto &response) { - if (response.data.empty()) - { - successCallback(false, HelixUsersFollowsRecord()); - return; - } - - successCallback(true, response.data[0]); - }, - std::move(failureCallback)); -} - void Helix::fetchStreams( QStringList userIds, QStringList userLogins, ResultCallback> successCallback, @@ -354,50 +335,6 @@ void Helix::getGameById(QString gameId, failureCallback); } -void Helix::followUser(QString userId, QString targetId, - std::function successCallback, - HelixFailureCallback failureCallback) -{ - QUrlQuery urlQuery; - - urlQuery.addQueryItem("from_id", userId); - urlQuery.addQueryItem("to_id", targetId); - - this->makeRequest("users/follows", urlQuery) - .type(NetworkRequestType::Post) - .onSuccess([successCallback](auto /*result*/) -> Outcome { - successCallback(); - return Success; - }) - .onError([failureCallback](auto /*result*/) { - // TODO: make better xd - failureCallback(); - }) - .execute(); -} - -void Helix::unfollowUser(QString userId, QString targetId, - std::function successCallback, - HelixFailureCallback failureCallback) -{ - QUrlQuery urlQuery; - - urlQuery.addQueryItem("from_id", userId); - urlQuery.addQueryItem("to_id", targetId); - - this->makeRequest("users/follows", urlQuery) - .type(NetworkRequestType::Delete) - .onSuccess([successCallback](auto /*result*/) -> Outcome { - successCallback(); - return Success; - }) - .onError([failureCallback](auto /*result*/) { - // TODO: make better xd - failureCallback(); - }) - .execute(); -} - void Helix::createClip(QString channelId, ResultCallback successCallback, std::function failureCallback, @@ -775,7 +712,7 @@ void Helix::getEmoteSetData(QString emoteSetId, QJsonObject root = result.parseJson(); auto data = root.value("data"); - if (!data.isArray()) + if (!data.isArray() || data.toArray().isEmpty()) { failureCallback(); return Failure; diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 0b437765c..7648243a2 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -341,11 +341,6 @@ public: ResultCallback successCallback, HelixFailureCallback failureCallback); - void getUserFollow( - QString userId, QString targetId, - ResultCallback successCallback, - HelixFailureCallback failureCallback); - // https://dev.twitch.tv/docs/api/reference#get-streams void fetchStreams(QStringList userIds, QStringList userLogins, ResultCallback> successCallback, @@ -372,16 +367,6 @@ public: void getGameById(QString gameId, ResultCallback successCallback, HelixFailureCallback failureCallback); - // https://dev.twitch.tv/docs/api/reference#create-user-follows - void followUser(QString userId, QString targetId, - std::function successCallback, - HelixFailureCallback failureCallback); - - // https://dev.twitch.tv/docs/api/reference#delete-user-follows - void unfollowUser(QString userId, QString targetlId, - std::function successCallback, - HelixFailureCallback failureCallback); - // https://dev.twitch.tv/docs/api/reference#create-clip void createClip(QString channelId, ResultCallback successCallback, diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 15c52cca9..6f1b46a03 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -47,26 +47,6 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams - `TwitchChannel` to get live status, game, title, and viewer count of a channel - `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for -### Follow User - -URL: https://dev.twitch.tv/docs/api/reference#create-user-follows -Requires `user:edit:follows` scope - -- We implement this in `providers/twitch/api/Helix.cpp followUser` - Used in: - - `widgets/dialogs/UserInfoPopup.cpp` to follow a user by ticking follow checkbox in usercard - - `controllers/commands/CommandController.cpp` in /follow command - -### Unfollow User - -URL: https://dev.twitch.tv/docs/api/reference#delete-user-follows -Requires `user:edit:follows` scope - -- We implement this in `providers/twitch/api/Helix.cpp unfollowUser` - Used in: - - `widgets/dialogs/UserInfoPopup.cpp` to unfollow a user by unticking follow checkbox in usercard - - `controllers/commands/CommandController.cpp` in /unfollow command - ### Create Clip URL: https://dev.twitch.tv/docs/api/reference#create-clip diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index e0fdff803..d647fd0d9 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -191,7 +191,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { auto app = getApp(); - QString action = root.value("action").toString(); if (action.isNull()) @@ -211,13 +210,20 @@ void NativeMessagingServer::ReceiverThread::handleMessage( AttachedWindow::GetArgs args; args.winId = root.value("winId").toString(); args.yOffset = root.value("yOffset").toInt(-1); - args.x = root.value("size").toObject().value("x").toInt(-1); - args.width = root.value("size").toObject().value("width").toInt(-1); - args.height = root.value("size").toObject().value("height").toInt(-1); + + { + const auto sizeObject = root.value("size").toObject(); + args.x = sizeObject.value("x").toDouble(-1.0); + args.pixelRatio = sizeObject.value("pixelRatio").toDouble(-1.0); + args.width = sizeObject.value("width").toInt(-1); + args.height = sizeObject.value("height").toInt(-1); + } + args.fullscreen = attachFullscreen; qCDebug(chatterinoNativeMessage) - << args.x << args.width << args.height << args.winId; + << args.x << args.pixelRatio << args.width << args.height + << args.winId; if (_type.isNull() || args.winId.isNull()) { diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index c7acb3319..f81620914 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -23,6 +23,7 @@ ConcurrentSettings::ConcurrentSettings() , ignoredMessages(*new SignalVector()) , mutedChannels(*new SignalVector()) , filterRecords(*new SignalVector()) + , nicknames(*new SignalVector()) , moderationActions(*new SignalVector) { persist(this->highlightedMessages, "/highlighting/highlights"); @@ -32,6 +33,7 @@ ConcurrentSettings::ConcurrentSettings() persist(this->ignoredMessages, "/ignore/phrases"); persist(this->mutedChannels, "/pings/muted"); persist(this->filterRecords, "/filtering/filters"); + persist(this->nicknames, "/nicknames"); // tagged users? persist(this->moderationActions, "/moderation/actions"); } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 808e7656b..703fefbde 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -10,6 +10,7 @@ #include "controllers/highlights/HighlightBadge.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" +#include "controllers/nicknames/Nickname.hpp" #include "singletons/Toasts.hpp" #include "util/StreamerMode.hpp" #include "widgets/Notebook.hpp" @@ -23,6 +24,7 @@ class HighlightBlacklistUser; class IgnorePhrase; class TaggedUser; class FilterRecord; +class Nickname; /// Settings which are availlable for reading on all threads. class ConcurrentSettings @@ -37,6 +39,7 @@ public: SignalVector &ignoredMessages; SignalVector &mutedChannels; SignalVector &filterRecords; + SignalVector &nicknames; //SignalVector &taggedUsers; SignalVector &moderationActions; diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 13e01bf87..2e73d1703 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -1,12 +1,15 @@ -#define LOOKUP_COLOR_COUNT 360 #include "singletons/Theme.hpp" + #include "Application.hpp" +#include "singletons/Resources.hpp" #include #include +#define LOOKUP_COLOR_COUNT 360 + namespace chatterino { Theme::Theme() @@ -80,6 +83,16 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.background = getColor(0, sat, 1); this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); + + // Copy button + if (this->isLightTheme()) + { + this->buttons.copy = getResources().buttons.copyDark; + } + else + { + this->buttons.copy = getResources().buttons.copyLight; + } } void Theme::normalizeColor(QColor &color) diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 99905fa83..8b55dbab2 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -48,6 +48,10 @@ public: } input; } splits; + struct { + QPixmap copy; + } buttons; + void normalizeColor(QColor &color); private: diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 540a13642..ef53e6814 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -27,6 +27,20 @@ public: reinterpret_cast(it.value())++; } } + static void increase(const QString &name, const int64_t &amount) + { + auto counts = counts_.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->insert(name, amount); + } + else + { + reinterpret_cast(it.value()) += amount; + } + } static void decrease(const QString &name) { @@ -42,6 +56,20 @@ public: reinterpret_cast(it.value())--; } } + static void decrease(const QString &name, const int64_t &amount) + { + auto counts = counts_.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->insert(name, -amount); + } + else + { + reinterpret_cast(it.value()) -= amount; + } + } static QString getDebugText() { diff --git a/src/util/RatelimitBucket.cpp b/src/util/RatelimitBucket.cpp new file mode 100644 index 000000000..c33f3a30a --- /dev/null +++ b/src/util/RatelimitBucket.cpp @@ -0,0 +1,45 @@ +#include "RatelimitBucket.hpp" + +#include + +namespace chatterino { + +RatelimitBucket::RatelimitBucket(int budget, int cooldown, + std::function callback, + QObject *parent) + : QObject(parent) + , budget_(budget) + , cooldown_(cooldown) + , callback_(callback) +{ +} + +void RatelimitBucket::send(QString channel) +{ + this->queue_.append(channel); + + if (this->budget_ > 0) + { + this->handleOne(); + } +} + +void RatelimitBucket::handleOne() +{ + if (queue_.isEmpty()) + { + return; + } + + auto item = queue_.takeFirst(); + + this->budget_--; + callback_(item); + + QTimer::singleShot(cooldown_, this, [this] { + this->budget_++; + this->handleOne(); + }); +} + +} // namespace chatterino diff --git a/src/util/RatelimitBucket.hpp b/src/util/RatelimitBucket.hpp new file mode 100644 index 000000000..89ecdc570 --- /dev/null +++ b/src/util/RatelimitBucket.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace chatterino { + +class RatelimitBucket : public QObject +{ +public: + RatelimitBucket(int budget, int cooldown, + std::function callback, QObject *parent); + + void send(QString channel); + +private: + /** + * @brief budget_ denotes the amount of calls that can be handled before we need to wait for the cooldown + **/ + int budget_; + + /** + * @brief This is the amount of time in milliseconds it takes for one used up budget to be put back into the bucket for use elsewhere + **/ + const int cooldown_; + + std::function callback_; + QList queue_; + + /** + * @brief Run the callback on one entry in the queue. + * + * This will start a timer that runs after cooldown_ milliseconds that + * gives back one "token" to the bucket and calls handleOne again. + **/ + void handleOne(); +}; + +} // namespace chatterino diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 15c1f6dc9..05392b2d2 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -93,6 +93,7 @@ AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) window->fullscreen_ = args.fullscreen; window->x_ = args.x; + window->pixelRatio_ = args.pixelRatio; if (args.height != -1) { @@ -276,7 +277,16 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) // offset int o = this->fullscreen_ ? 0 : 8; - if (this->x_ != -1) + if (this->pixelRatio_ != -1.0) + { + ::MoveWindow( + hwnd, + int(rect.left + this->x_ * scale * this->pixelRatio_ + o - 2), + int(rect.bottom - this->height_ * scale - o), + int(this->width_ * scale), int(this->height_ * scale), true); + } + //support for old extension version 1.3 + else if (this->x_ != -1.0) { ::MoveWindow(hwnd, int(rect.left + this->x_ * scale + o), int(rect.bottom - this->height_ * scale - o), diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index 4ecefa11c..186d310fd 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -17,7 +17,8 @@ public: struct GetArgs { QString winId; int yOffset = -1; - int x = -1; + double x = -1; + double pixelRatio = -1; int width = -1; int height = -1; bool fullscreen = false; @@ -54,7 +55,8 @@ private: void *target_; int yOffset_; int currentYOffset_; - int x_ = -1; + double x_ = -1; + double pixelRatio_ = -1; int width_ = 360; int height_ = -1; bool fullscreen_ = false; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 649f2f6da..6cc7c69a3 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -641,11 +641,7 @@ 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/FramelessEmbedWindow.cpp b/src/widgets/FramelessEmbedWindow.cpp index 263daf78b..f34759f81 100644 --- a/src/widgets/FramelessEmbedWindow.cpp +++ b/src/widgets/FramelessEmbedWindow.cpp @@ -30,11 +30,7 @@ FramelessEmbedWindow::FramelessEmbedWindow() bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) { -# if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1)) - MSG *msg = *reinterpret_cast(message); -# else MSG *msg = reinterpret_cast(message); -# endif if (msg->message == WM_COPYDATA) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index ccd5fbee8..47db42b4d 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -17,6 +17,7 @@ #include "widgets/settingspages/IgnoresPage.hpp" #include "widgets/settingspages/KeyboardSettingsPage.hpp" #include "widgets/settingspages/ModerationPage.hpp" +#include "widgets/settingspages/NicknamesPage.hpp" #include "widgets/settingspages/NotificationPage.hpp" #include @@ -164,6 +165,7 @@ void SettingsDialog::addTabs() this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg"); this->ui_.tabContainer->addSpacing(16); this->addTab([]{return new AccountsPage;}, "Accounts", ":/settings/accounts.svg", SettingsTabId::Accounts); + this->addTab([]{return new NicknamesPage;}, "Nicknames", ":/settings/accounts.svg"); this->ui_.tabContainer->addSpacing(16); this->addTab([]{return new CommandPage;}, "Commands", ":/settings/commands.svg"); this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg"); diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 3c82d43f4..706ca839d 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -13,6 +13,7 @@ #include "providers/twitch/api/Kraken.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "util/Clipboard.hpp" #include "util/Helpers.hpp" #include "util/LayoutCreator.hpp" @@ -42,7 +43,7 @@ namespace { { auto label = box.emplace