Merge remote-tracking branch 'origin/master' into zneix/enhancement/login-overhaul

This commit is contained in:
zneix 2021-08-08 21:46:28 +02:00
commit bdf78c1f5c
No known key found for this signature in database
GPG key ID: 911916E0523B22F6
84 changed files with 1182 additions and 555 deletions

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -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!

1
.gitmodules vendored
View file

@ -1,6 +1,7 @@
[submodule "lib/libcommuni"] [submodule "lib/libcommuni"]
path = lib/libcommuni path = lib/libcommuni
url = https://github.com/Chatterino/libcommuni url = https://github.com/Chatterino/libcommuni
branch = chatterino-cmake
[submodule "lib/qBreakpad"] [submodule "lib/qBreakpad"]
path = lib/qBreakpad path = lib/qBreakpad
url = https://github.com/jiakuan/qBreakpad.git url = https://github.com/jiakuan/qBreakpad.git

View file

@ -10,6 +10,7 @@ FreeBSD 13.0-CURRENT.
1. Install build dependencies from package sources (or build from the 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` 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. Go into the project directory
1. create build folder `$ mkdir build && cd build` 1. Create a build folder and go into it (`mkdir build && cd build`)
1. `$ qmake .. && make` 1. Proceed to compiling using the command
`qmake .. && make`

View file

@ -4,27 +4,28 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
## Ubuntu 18.04 ## 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 1. Open `chatterino.pro` with Qt Creator and select build
### Manually ### Manually
1. go into project directory 1. Go into the project directory
1. create build folder `mkdir build && cd build` 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 ## Arch Linux
@ -34,50 +35,47 @@ _most likely works the same for other Debian-like distros_
### Manually ### 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. 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 project directory 1. Go into the project directory
1. create build folder `mkdir build && cd build` 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 ## 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. 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 project directory 1. Go into the project directory
1. create build folder `mkdir build && cd build` 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
1. `qmake-qt5 .. && make -j$(nproc)`
### Using CMake ### 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)_ `qmake-qt5 .. && make -j$(nproc)`
1. `sudo dnf install gstreamer-plugins-good` _(optional: for audio output)_
## NixOS 18.09+ ## NixOS 18.09+
1. enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake` 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. Go into the project directory
1. create build folder `mkdir build && cd build` 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
1. `qmake .. && make`
### Using CMake ### Using CMake
1. `cmake .. && make` `cmake .. && make`
### Using QMake
`qmake .. && make`

View file

@ -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 #### 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 1. Install Xcode and Xcode Command Line Utilities
2. Start Xcode, settings -> Locations, activate your Command Line Tools 2. Start Xcode, go into Settings -> Locations, and activate your Command Line Tools
3. Install brew https://brew.sh/ 3. Install brew https://brew.sh/
4. `brew install boost openssl rapidjson` 4. Install the dependencies using `brew install boost openssl rapidjson`
5. `brew install qt` 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 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 7. Go into the project directory
8. Create build folder `mkdir build && cd build` 8. Create a build folder and go into it (`mkdir build && cd build`)
9. `qmake .. && make` 9. Compile using `qmake .. && make`
_If you want to use cmake instead of qmake, just replace the above qmake command with cmake_ _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 openssl`
`brew info boost` `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/lib/* /usr/local/lib`
- `ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl` - `ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl`

View file

@ -1,8 +1,8 @@
# Building on Windows # 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 ## 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 ### 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. 2. When prompted, install it to any arbitrary empty directory.
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) 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) 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) 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.** **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 ## 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: 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) ![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". 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): 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 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`. 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 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 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)). 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` 1. `mkdir build`
2. `cd build` 2. `cd build`

View file

@ -3,13 +3,34 @@
## Unversioned ## 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) - 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: 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: 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 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: 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 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 "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 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: Ubuntu packages are now available (#2936)
- Dev: Disabled update checker on Flatpak. (#3051) - Dev: Disabled update checker on Flatpak. (#3051)
- Dev: Add logging for HTTP requests (#2991) - Dev: Add logging for HTTP requests (#2991)

View file

@ -7,17 +7,25 @@ list(APPEND CMAKE_MODULE_PATH
"${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" "${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_APP "Build Chatterino" ON)
option(BUILD_TESTS "Build the tests for Chatterino" OFF) 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_PAJLADA_SETTINGS "Use system pajlada settings library" OFF)
option(USE_SYSTEM_LIBCOMMUNI "Use system communi 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(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF)
option(USE_CONAN "Use conan" 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) if (USE_CONAN OR CONAN_EXPORTED)
include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake) include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS NO_OUTPUT_DIRS) conan_basic_setup(TARGETS NO_OUTPUT_DIRS)
@ -31,7 +39,7 @@ endif ()
include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake)
find_package(Qt5 REQUIRED find_package(Qt${MAJOR_QT_VERSION} REQUIRED
COMPONENTS COMPONENTS
Core Core
Widgets Widgets
@ -73,17 +81,17 @@ endif()
# Link QtKeychain statically # Link QtKeychain statically
option(QTKEYCHAIN_STATIC "" ON) option(QTKEYCHAIN_STATIC "" ON)
if (USE_SYSTEM_QT5KEYCHAIN) if (USE_SYSTEM_QTKEYCHAIN)
find_package(Qt5Keychain REQUIRED) find_package(Qt${MAJOR_QT_VERSION}Keychain REQUIRED)
else() else()
set(QT5KEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain") set(QTKEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain")
if (NOT EXISTS "${QT5KEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt") if (NOT EXISTS "${QTKEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt")
message(FATAL_ERROR "Submodules probably not loaded, unable to find lib/qtkeychain/CMakeLists.txt") message(FATAL_ERROR "Submodules probably not loaded, unable to find lib/qtkeychain/CMakeLists.txt")
endif() endif()
add_subdirectory("${QT5KEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL) add_subdirectory("${QTKEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL)
if (NOT TARGET qt5keychain) if (NOT TARGET qt${MAJOR_QT_VERSION}keychain)
message(FATAL_ERROR "qt5keychain target was not created :@") message(FATAL_ERROR "qt${MAJOR_QT_VERSION}keychain target was not created :@")
endif() endif()
endif() endif()
@ -95,6 +103,11 @@ if (BUILD_TESTS)
find_package(GTest REQUIRED) find_package(GTest REQUIRED)
endif () endif ()
if (BUILD_BENCHMARKS)
# Include system benchmark (Google Benchmark)
find_package(benchmark REQUIRED)
endif ()
find_package(PajladaSerialize REQUIRED) find_package(PajladaSerialize REQUIRED)
find_package(PajladaSignals REQUIRED) find_package(PajladaSignals REQUIRED)
find_package(LRUCache REQUIRED) find_package(LRUCache REQUIRED)
@ -112,7 +125,7 @@ endif()
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (BUILD_TESTS) if (BUILD_TESTS OR BUILD_BENCHMARKS)
add_definitions(-DCHATTERINO_TEST) add_definitions(-DCHATTERINO_TEST)
endif () endif ()
@ -123,4 +136,8 @@ if (BUILD_TESTS)
add_subdirectory(tests) add_subdirectory(tests)
endif () endif ()
if (BUILD_BENCHMARKS)
add_subdirectory(benchmarks)
endif ()
feature_summary(WHAT ALL) feature_summary(WHAT ALL)

View file

@ -1,6 +1,6 @@
# Chatterino code guidelines # 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 # Tooling
@ -70,7 +70,7 @@ void myFunc() {
## Passing parameters ## 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. **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<Channel>&` (aka `ChannelPtr`) if possible. Generally the lowest level of requirement should be used, e.g. passing `Channel&` instead of `std::shared_ptr<Channel>&` (aka `ChannelPtr`) if possible.
## Members ## 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 ```cpp
class NamedObject class NamedObject
@ -212,6 +212,6 @@ Keep the element on the stack if possible. If you need a pointer or have complex
#### QObject classes #### 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 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<Type, DeleteLater>` 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<Type, DeleteLater>` 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.

35
benchmarks/.clang-format Normal file
View file

@ -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

28
benchmarks/CMakeLists.txt Normal file
View file

@ -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"
)

57
benchmarks/src/Emojis.cpp Normal file
View file

@ -0,0 +1,57 @@
#include "providers/emoji/Emojis.hpp"
#include <benchmark/benchmark.h>
#include <QDebug>
#include <QString>
using namespace chatterino;
static void BM_ShortcodeParsing(benchmark::State &state)
{
Emojis emojis;
emojis.load();
struct TestCase {
QString input;
QString expectedOutput;
};
std::vector<TestCase> 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);

18
benchmarks/src/main.cpp Normal file
View file

@ -0,0 +1,18 @@
#include <benchmark/benchmark.h>
#include <QApplication>
#include <QtConcurrent>
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();
}

View file

@ -14,7 +14,7 @@ CCACHE_BIN = $$system(which ccache)
CONFIG+=ccache CONFIG+=ccache
} }
MINIMUM_REQUIRED_QT_VERSION = 5.12.0 MINIMUM_REQUIRED_QT_VERSION = 5.11.0
!versionAtLeast(QT_VERSION, $$MINIMUM_REQUIRED_QT_VERSION) { !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") 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/HighlightModel.cpp \
src/controllers/highlights/HighlightPhrase.cpp \ src/controllers/highlights/HighlightPhrase.cpp \
src/controllers/highlights/UserHighlightModel.cpp \ src/controllers/highlights/UserHighlightModel.cpp \
src/controllers/ignores/IgnoreController.cpp \
src/controllers/ignores/IgnoreModel.cpp \ src/controllers/ignores/IgnoreModel.cpp \
src/controllers/moderationactions/ModerationAction.cpp \ src/controllers/moderationactions/ModerationAction.cpp \
src/controllers/moderationactions/ModerationActionModel.cpp \ src/controllers/moderationactions/ModerationActionModel.cpp \
src/controllers/nicknames/NicknamesModel.cpp \
src/controllers/notifications/NotificationController.cpp \ src/controllers/notifications/NotificationController.cpp \
src/controllers/notifications/NotificationModel.cpp \ src/controllers/notifications/NotificationModel.cpp \
src/controllers/pings/MutedChannelModel.cpp \ src/controllers/pings/MutedChannelModel.cpp \
@ -250,6 +252,7 @@ SOURCES += \
src/util/LayoutHelper.cpp \ src/util/LayoutHelper.cpp \
src/util/NuulsUploader.cpp \ src/util/NuulsUploader.cpp \
src/util/RapidjsonHelpers.cpp \ src/util/RapidjsonHelpers.cpp \
src/util/RatelimitBucket.cpp \
src/util/SplitCommand.cpp \ src/util/SplitCommand.cpp \
src/util/StreamerMode.cpp \ src/util/StreamerMode.cpp \
src/util/StreamLink.cpp \ src/util/StreamLink.cpp \
@ -316,6 +319,7 @@ SOURCES += \
src/widgets/settingspages/IgnoresPage.cpp \ src/widgets/settingspages/IgnoresPage.cpp \
src/widgets/settingspages/KeyboardSettingsPage.cpp \ src/widgets/settingspages/KeyboardSettingsPage.cpp \
src/widgets/settingspages/ModerationPage.cpp \ src/widgets/settingspages/ModerationPage.cpp \
src/widgets/settingspages/NicknamesPage.cpp \
src/widgets/settingspages/NotificationPage.cpp \ src/widgets/settingspages/NotificationPage.cpp \
src/widgets/settingspages/SettingsPage.cpp \ src/widgets/settingspages/SettingsPage.cpp \
src/widgets/splits/ClosedSplits.cpp \ src/widgets/splits/ClosedSplits.cpp \
@ -392,6 +396,8 @@ HEADERS += \
src/controllers/ignores/IgnorePhrase.hpp \ src/controllers/ignores/IgnorePhrase.hpp \
src/controllers/moderationactions/ModerationAction.hpp \ src/controllers/moderationactions/ModerationAction.hpp \
src/controllers/moderationactions/ModerationActionModel.hpp \ src/controllers/moderationactions/ModerationActionModel.hpp \
src/controllers/nicknames/Nickname.hpp \
src/controllers/nicknames/NicknamesModel.hpp \
src/controllers/notifications/NotificationController.hpp \ src/controllers/notifications/NotificationController.hpp \
src/controllers/notifications/NotificationModel.hpp \ src/controllers/notifications/NotificationModel.hpp \
src/controllers/pings/MutedChannelModel.hpp \ src/controllers/pings/MutedChannelModel.hpp \
@ -505,6 +511,7 @@ HEADERS += \
src/util/rangealgorithm.hpp \ src/util/rangealgorithm.hpp \
src/util/RapidjsonHelpers.hpp \ src/util/RapidjsonHelpers.hpp \
src/util/RapidJsonSerializeQString.hpp \ src/util/RapidJsonSerializeQString.hpp \
src/util/RatelimitBucket.hpp \
src/util/RemoveScrollAreaBackground.hpp \ src/util/RemoveScrollAreaBackground.hpp \
src/util/SampleCheerMessages.hpp \ src/util/SampleCheerMessages.hpp \
src/util/SampleLinks.hpp \ src/util/SampleLinks.hpp \
@ -580,6 +587,7 @@ HEADERS += \
src/widgets/settingspages/IgnoresPage.hpp \ src/widgets/settingspages/IgnoresPage.hpp \
src/widgets/settingspages/KeyboardSettingsPage.hpp \ src/widgets/settingspages/KeyboardSettingsPage.hpp \
src/widgets/settingspages/ModerationPage.hpp \ src/widgets/settingspages/ModerationPage.hpp \
src/widgets/settingspages/NicknamesPage.hpp \
src/widgets/settingspages/NotificationPage.hpp \ src/widgets/settingspages/NotificationPage.hpp \
src/widgets/settingspages/SettingsPage.hpp \ src/widgets/settingspages/SettingsPage.hpp \
src/widgets/splits/ClosedSplits.hpp \ src/widgets/splits/ClosedSplits.hpp \

View file

@ -1,6 +1,6 @@
[requires] [requires]
openssl/1.1.1d openssl/1.1.1k
boost/1.75.0 boost/1.76.0
[generators] [generators]
qmake qmake

View file

@ -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
```

@ -1 +1 @@
Subproject commit ef8daa14946b8e19f536200e28db2b25e8311ba5 Subproject commit 95f05478de1623767282d8019ea8f3a4b1178b35

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1,004 B

View file

@ -9,5 +9,5 @@
height="368.64pt" height="368.64pt"
viewBox="0 0 368.64 368.64"> viewBox="0 0 368.64 368.64">
<defs/> <defs/>
<path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#ffffff" fill-rule="evenodd" stroke="#000000" stroke-opacity="0" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#f2f2f2" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#ffffff" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/> <path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#000000" fill-rule="evenodd" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#000000" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#000000" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,004 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -9,5 +9,5 @@
height="368.64pt" height="368.64pt"
viewBox="0 0 368.64 368.64"> viewBox="0 0 368.64 368.64">
<defs/> <defs/>
<path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#000000" fill-rule="evenodd" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#000000" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#000000" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/> <path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#ffffff" fill-rule="evenodd" stroke="#000000" stroke-opacity="0" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#f2f2f2" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#ffffff" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -32,6 +32,6 @@
<binary>chatterino</binary> <binary>chatterino</binary>
</provides> </provides>
<releases> <releases>
<release version="2.3.3" date="2021-06-21"/> <release version="2.3.4" date="2021-08-05"/>
</releases> </releases>
</component> </component>

View file

@ -43,6 +43,8 @@ matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg | Contributor
Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor
Talen | https://github.com/talneoran | | Contributor Talen | https://github.com/talneoran | | Contributor
SLCH | https://github.com/SLCH | :/avatars/slch.png | 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 # If you are a contributor add yourself above this line

View file

@ -1,6 +1,7 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>auth.html</file> <file>auth.html</file>
<file>avatars/alazymeme.png</file>
<file>avatars/fourtf.png</file> <file>avatars/fourtf.png</file>
<file>avatars/kararty.png</file> <file>avatars/kararty.png</file>
<file>avatars/matthewde.jpg</file> <file>avatars/matthewde.jpg</file>
@ -8,6 +9,7 @@
<file>avatars/pajlada.png</file> <file>avatars/pajlada.png</file>
<file>avatars/revolter.jpg</file> <file>avatars/revolter.jpg</file>
<file>avatars/slch.png</file> <file>avatars/slch.png</file>
<file>avatars/xheaveny.png</file>
<file>avatars/zneix.png</file> <file>avatars/zneix.png</file>
<file>buttons/addSplit.png</file> <file>buttons/addSplit.png</file>
<file>buttons/addSplitDark.png</file> <file>buttons/addSplitDark.png</file>

View file

@ -88,6 +88,8 @@ set(SOURCE_FILES
controllers/highlights/UserHighlightModel.cpp controllers/highlights/UserHighlightModel.cpp
controllers/highlights/UserHighlightModel.hpp controllers/highlights/UserHighlightModel.hpp
controllers/ignores/IgnoreController.cpp
controllers/ignores/IgnoreController.hpp
controllers/ignores/IgnoreModel.cpp controllers/ignores/IgnoreModel.cpp
controllers/ignores/IgnoreModel.hpp controllers/ignores/IgnoreModel.hpp
@ -96,6 +98,10 @@ set(SOURCE_FILES
controllers/moderationactions/ModerationActionModel.cpp controllers/moderationactions/ModerationActionModel.cpp
controllers/moderationactions/ModerationActionModel.hpp controllers/moderationactions/ModerationActionModel.hpp
controllers/nicknames/NicknamesModel.cpp
controllers/nicknames/NicknamesModel.hpp
controllers/nicknames/Nickname.hpp
controllers/notifications/NotificationController.cpp controllers/notifications/NotificationController.cpp
controllers/notifications/NotificationController.hpp controllers/notifications/NotificationController.hpp
controllers/notifications/NotificationModel.cpp controllers/notifications/NotificationModel.cpp
@ -286,6 +292,8 @@ set(SOURCE_FILES
util/NuulsUploader.hpp util/NuulsUploader.hpp
util/RapidjsonHelpers.cpp util/RapidjsonHelpers.cpp
util/RapidjsonHelpers.hpp util/RapidjsonHelpers.hpp
util/RatelimitBucket.cpp
util/RatelimitBucket.hpp
util/SplitCommand.cpp util/SplitCommand.cpp
util/SplitCommand.hpp util/SplitCommand.hpp
util/StreamLink.cpp util/StreamLink.cpp
@ -430,6 +438,8 @@ set(SOURCE_FILES
widgets/settingspages/KeyboardSettingsPage.hpp widgets/settingspages/KeyboardSettingsPage.hpp
widgets/settingspages/ModerationPage.cpp widgets/settingspages/ModerationPage.cpp
widgets/settingspages/ModerationPage.hpp widgets/settingspages/ModerationPage.hpp
widgets/settingspages/NicknamesPage.cpp
widgets/settingspages/NicknamesPage.hpp
widgets/settingspages/NotificationPage.cpp widgets/settingspages/NotificationPage.cpp
widgets/settingspages/NotificationPage.hpp widgets/settingspages/NotificationPage.hpp
widgets/settingspages/SettingsPage.cpp widgets/settingspages/SettingsPage.cpp
@ -478,17 +488,17 @@ add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES})
target_link_libraries(${LIBRARY_PROJECT} target_link_libraries(${LIBRARY_PROJECT}
PUBLIC PUBLIC
Qt5::Core Qt${MAJOR_QT_VERSION}::Core
Qt5::Widgets Qt${MAJOR_QT_VERSION}::Widgets
Qt5::Gui Qt${MAJOR_QT_VERSION}::Gui
Qt5::Network Qt${MAJOR_QT_VERSION}::Network
Qt5::Multimedia Qt${MAJOR_QT_VERSION}::Multimedia
Qt5::Svg Qt${MAJOR_QT_VERSION}::Svg
Qt5::Concurrent Qt${MAJOR_QT_VERSION}::Concurrent
Qt5::HttpServer Qt${MAJOR_QT_VERSION}::HttpServer
LibCommuni::LibCommuni LibCommuni::LibCommuni
qt5keychain qt${MAJOR_QT_VERSION}keychain
Pajlada::Serialize Pajlada::Serialize
Pajlada::Settings Pajlada::Settings
Pajlada::Signals Pajlada::Signals
@ -517,8 +527,8 @@ if (BUILD_APP)
) )
if (MSVC) if (MSVC)
get_target_property(Qt5_Core_Location Qt5::Core LOCATION) get_target_property(Qt_Core_Location Qt${MAJOR_QT_VERSION}::Core LOCATION)
get_filename_component(QT_BIN_DIR ${Qt5_Core_Location} DIRECTORY) get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY)
set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $<TARGET_FILE:${EXECUTABLE_PROJECT}> --release --no-compiler-runtime --no-translations --no-opengl-sw) set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $<TARGET_FILE:${EXECUTABLE_PROJECT}> --release --no-compiler-runtime --no-translations --no-opengl-sw)
install(TARGETS ${EXECUTABLE_PROJECT} install(TARGETS ${EXECUTABLE_PROJECT}
@ -575,7 +585,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\" CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\"
CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\" CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\"
) )
if (USE_SYSTEM_QT5KEYCHAIN) if (USE_SYSTEM_QTKEYCHAIN)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CMAKE_BUILD CMAKE_BUILD
) )

View file

@ -4,11 +4,13 @@ namespace chatterino {
Resources2::Resources2() Resources2::Resources2()
{ {
this->avatars.alazymeme = QPixmap(":/avatars/alazymeme.png");
this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); this->avatars.fourtf = QPixmap(":/avatars/fourtf.png");
this->avatars.kararty = QPixmap(":/avatars/kararty.png"); this->avatars.kararty = QPixmap(":/avatars/kararty.png");
this->avatars.mm2pl = QPixmap(":/avatars/mm2pl.png"); this->avatars.mm2pl = QPixmap(":/avatars/mm2pl.png");
this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); this->avatars.pajlada = QPixmap(":/avatars/pajlada.png");
this->avatars.slch = QPixmap(":/avatars/slch.png"); this->avatars.slch = QPixmap(":/avatars/slch.png");
this->avatars.xheaveny = QPixmap(":/avatars/xheaveny.png");
this->avatars.zneix = QPixmap(":/avatars/zneix.png"); this->avatars.zneix = QPixmap(":/avatars/zneix.png");
this->buttons.addSplit = QPixmap(":/buttons/addSplit.png"); this->buttons.addSplit = QPixmap(":/buttons/addSplit.png");
this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png"); this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png");

View file

@ -9,11 +9,13 @@ public:
Resources2(); Resources2();
struct { struct {
QPixmap alazymeme;
QPixmap fourtf; QPixmap fourtf;
QPixmap kararty; QPixmap kararty;
QPixmap mm2pl; QPixmap mm2pl;
QPixmap pajlada; QPixmap pajlada;
QPixmap slch; QPixmap slch;
QPixmap xheaveny;
QPixmap zneix; QPixmap zneix;
} avatars; } avatars;
struct { struct {

View file

@ -3,7 +3,7 @@
#include <QString> #include <QString>
#include <QtGlobal> #include <QtGlobal>
#define CHATTERINO_VERSION "2.3.3" #define CHATTERINO_VERSION "2.3.4"
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
# define CHATTERINO_OS "win" # define CHATTERINO_OS "win"

View file

@ -70,6 +70,32 @@ static const QStringList twitchDefaultCommands{
static const QStringList whisperCommands{"/w", ".w"}; 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) void sendWhisperMessage(const QString &text)
{ {
// (hemirt) pajlada: "we should not be sending whispers through jtv, but // (hemirt) pajlada: "we should not be sending whispers through jtv, but
@ -373,6 +399,22 @@ void CommandController::initialize(Settings &, Paths &paths)
return ""; 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 /// Supported commands
this->registerCommand( this->registerCommand(
@ -407,90 +449,6 @@ void CommandController::initialize(Settings &, Paths &paths)
this->registerCommand("/unblock", unblockLambda); 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) { this->registerCommand("/user", [](const auto &words, auto channel) {
if (words.size() < 2) if (words.size() < 2)
{ {
@ -498,16 +456,17 @@ void CommandController::initialize(Settings &, Paths &paths)
makeSystemMessage("Usage /user [user] (channel)")); makeSystemMessage("Usage /user [user] (channel)"));
return ""; return "";
} }
QString userName = words[1];
stripUserName(userName);
QString channelName = channel->getName(); QString channelName = channel->getName();
if (words.size() > 2) if (words.size() > 2)
{ {
channelName = words[2]; channelName = words[2];
if (channelName[0] == '#') stripChannelName(channelName);
{
channelName.remove(0, 1);
} }
} openTwitchUsercard(channelName, userName);
openTwitchUsercard(channelName, words[1]);
return ""; return "";
}); });
@ -519,10 +478,12 @@ void CommandController::initialize(Settings &, Paths &paths)
return ""; return "";
} }
QString userName = words[1];
stripUserName(userName);
auto *userPopup = new UserInfoPopup( auto *userPopup = new UserInfoPopup(
getSettings()->autoCloseUserPopup, getSettings()->autoCloseUserPopup,
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()))); static_cast<QWidget *>(&(getApp()->windows->getMainWindow())));
userPopup->setData(words[1], channel); userPopup->setData(userName, channel);
userPopup->move(QCursor::pos()); userPopup->move(QCursor::pos());
userPopup->show(); userPopup->show();
return ""; return "";

View file

@ -60,11 +60,6 @@ public:
return this->parser_->valid(); return this->parser_->valid();
} }
bool filter(const MessagePtr &message) const
{
return this->parser_->execute(message);
}
bool filter(const filterparser::ContextMap &context) const bool filter(const filterparser::ContextMap &context) const
{ {
return this->parser_->execute(context); return this->parser_->execute(context);

View file

@ -36,12 +36,13 @@ public:
this->listener_.disconnect(); this->listener_.disconnect();
} }
bool filter(const MessagePtr &m) const bool filter(const MessagePtr &m, ChannelPtr channel) const
{ {
if (this->filters_.size() == 0) if (this->filters_.size() == 0)
return true; return true;
filterparser::ContextMap context = filterparser::buildContextMap(m); filterparser::ContextMap context =
filterparser::buildContextMap(m, channel.get());
for (const auto &f : this->filters_.values()) for (const auto &f : this->filters_.values())
{ {
if (!f->valid() || !f->filter(context)) if (!f->valid() || !f->filter(context))

View file

@ -1,12 +1,13 @@
#include "FilterParser.hpp" #include "FilterParser.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "common/Channel.hpp"
#include "controllers/filters/parser/Types.hpp" #include "controllers/filters/parser/Types.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
namespace filterparser { namespace filterparser {
ContextMap buildContextMap(const MessagePtr &m) ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
{ {
auto watchingChannel = auto watchingChannel =
chatterino::getApp()->twitch.server->watchingChannel.get(); chatterino::getApp()->twitch.server->watchingChannel.get();
@ -61,8 +62,7 @@ ContextMap buildContextMap(const MessagePtr &m)
subLength = m->badgeInfos.at(subBadge).toInt(); subLength = m->badgeInfos.at(subBadge).toInt();
} }
} }
ContextMap vars = {
return {
{"author.badges", std::move(badges)}, {"author.badges", std::move(badges)},
{"author.color", m->usernameColor}, {"author.color", m->usernameColor},
{"author.name", m->displayName}, {"author.name", m->displayName},
@ -82,6 +82,19 @@ ContextMap buildContextMap(const MessagePtr &m)
{"message.content", m->messageText}, {"message.content", m->messageText},
{"message.length", m->messageText.length()}, {"message.length", m->messageText.length()},
}; };
{
using namespace chatterino;
auto *tc = dynamic_cast<TwitchChannel *>(channel);
if (channel && !channel->isEmpty() && tc)
{
vars["channel.live"] = tc->isLive();
}
else
{
vars["channel.live"] = false;
}
}
return vars;
} }
FilterParser::FilterParser(const QString &text) 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 bool FilterParser::execute(const ContextMap &context) const
{ {
return this->builtExpression_->execute(context).toBool(); return this->builtExpression_->execute(context).toBool();

View file

@ -3,15 +3,20 @@
#include "controllers/filters/parser/Tokenizer.hpp" #include "controllers/filters/parser/Tokenizer.hpp"
#include "controllers/filters/parser/Types.hpp" #include "controllers/filters/parser/Types.hpp"
namespace chatterino {
class Channel;
} // namespace chatterino
namespace filterparser { namespace filterparser {
ContextMap buildContextMap(const MessagePtr &m); ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
class FilterParser class FilterParser
{ {
public: public:
FilterParser(const QString &text); FilterParser(const QString &text);
bool execute(const MessagePtr &message) const;
bool execute(const ContextMap &context) const; bool execute(const ContextMap &context) const;
bool valid() const; bool valid() const;

View file

@ -17,6 +17,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
{"author.sub_length", "author sub length"}, {"author.sub_length", "author sub length"},
{"channel.name", "channel name"}, {"channel.name", "channel name"},
{"channel.watching", "/watching channel?"}, {"channel.watching", "/watching channel?"},
{"channel.live", "Channel live?"},
{"flags.highlighted", "highlighted?"}, {"flags.highlighted", "highlighted?"},
{"flags.points_redeemed", "redeemed points?"}, {"flags.points_redeemed", "redeemed points?"},
{"flags.sub_message", "sub/resub message?"}, {"flags.sub_message", "sub/resub message?"},

View file

@ -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 &&params)
{
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<ShowIgnoredUsersMessages>(
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

View file

@ -1,7 +1,19 @@
#pragma once #pragma once
#include <QString>
namespace chatterino { namespace chatterino {
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster }; enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
struct IgnoredMessageParameters {
QString message;
QString twitchUserID;
bool isMod;
bool isBroadcaster;
};
bool isIgnoredMessage(IgnoredMessageParameters &&params);
} // namespace chatterino } // namespace chatterino

View file

@ -0,0 +1,77 @@
#pragma once
#include "controllers/accounts/AccountController.hpp"
#include "util/RapidJsonSerializeQString.hpp"
#include "util/RapidjsonHelpers.hpp"
#include <QString>
#include <pajlada/serialize.hpp>
#include <memory>
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<chatterino::Nickname> {
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<chatterino::Nickname> {
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

View file

@ -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<Nickname>(2, parent)
{
}
// turn a vector item into a model row
Nickname NicknamesModel::getItemFromRow(std::vector<QStandardItem *> &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<QStandardItem *> &row)
{
setStringItem(row[0], item.name());
setStringItem(row[1], item.replace());
}
} // namespace chatterino

View file

@ -0,0 +1,25 @@
#pragma once
#include <QObject>
#include "common/SignalVectorModel.hpp"
#include "controllers/nicknames/Nickname.hpp"
namespace chatterino {
class NicknamesModel : public SignalVectorModel<Nickname>
{
public:
explicit NicknamesModel(QObject *parent);
protected:
// turn a vector item into a model row
virtual Nickname getItemFromRow(std::vector<QStandardItem *> &row,
const Nickname &original) override;
// turns a row in the model into a vector item
virtual void getRowFromItem(const Nickname &item,
std::vector<QStandardItem *> &row) override;
};
} // namespace chatterino

View file

@ -2,6 +2,7 @@
#include "Application.hpp" #include "Application.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "controllers/ignores/IgnoreController.hpp"
#include "controllers/ignores/IgnorePhrase.hpp" #include "controllers/ignores/IgnorePhrase.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
@ -104,20 +105,9 @@ void SharedMessageBuilder::parse()
bool SharedMessageBuilder::isIgnored() const bool SharedMessageBuilder::isIgnored() const
{ {
// TODO(pajlada): Do we need to check if the phrase is valid first? return isIgnoredMessage({
auto phrases = getCSettings().ignoredMessages.readOnly(); /*.message = */ this->originalMessage_,
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;
} }
void SharedMessageBuilder::parseUsernameColor() void SharedMessageBuilder::parseUsernameColor()

View file

@ -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 // 60 falloff counter means it will try to reconnect at most every 60*2 seconds
const int MAX_FALLOFF_COUNTER = 60; const int MAX_FALLOFF_COUNTER = 60;
// Ratelimits for joinBucket_
const int JOIN_RATELIMIT_BUDGET = 18;
const int JOIN_RATELIMIT_COOLDOWN = 10500;
AbstractIrcServer::AbstractIrcServer() AbstractIrcServer::AbstractIrcServer()
{ {
// Initialize the connections // Initialize the connections
@ -23,6 +27,17 @@ AbstractIrcServer::AbstractIrcServer()
this->writeConnection_->moveToThread( this->writeConnection_->moveToThread(
QCoreApplication::instance()->thread()); 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(), QObject::connect(this->writeConnection_.get(),
&Communi::IrcConnection::messageReceived, this, &Communi::IrcConnection::messageReceived, this,
[this](auto msg) { [this](auto msg) {
@ -214,11 +229,6 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
{ {
this->readConnection_->sendRaw("PART #" + channelName); this->readConnection_->sendRaw("PART #" + channelName);
} }
if (this->writeConnection_ && this->hasSeparateWriteConnection())
{
this->writeConnection_->sendRaw("PART #" + channelName);
}
})); }));
// join irc channel // join irc channel
@ -229,15 +239,7 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
{ {
if (this->readConnection_->isConnected()) if (this->readConnection_->isConnected())
{ {
this->readConnection_->sendRaw("JOIN #" + channelName); this->joinBucket_->send(channelName);
}
}
if (this->writeConnection_ && this->hasSeparateWriteConnection())
{
if (this->readConnection_->isConnected())
{
this->writeConnection_->sendRaw("JOIN #" + channelName);
} }
} }
} }
@ -297,7 +299,7 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
{ {
if (auto channel = weak.lock()) if (auto channel = weak.lock())
{ {
connection->sendRaw("JOIN #" + channel->getName()); this->joinBucket_->send(channel->getName());
} }
} }

View file

@ -8,6 +8,7 @@
#include "common/Common.hpp" #include "common/Common.hpp"
#include "providers/irc/IrcConnection2.hpp" #include "providers/irc/IrcConnection2.hpp"
#include "util/RatelimitBucket.hpp"
namespace chatterino { namespace chatterino {
@ -88,6 +89,10 @@ private:
QObjectPtr<IrcConnection> writeConnection_ = nullptr; QObjectPtr<IrcConnection> writeConnection_ = nullptr;
QObjectPtr<IrcConnection> readConnection_ = nullptr; QObjectPtr<IrcConnection> readConnection_ = nullptr;
// Our rate limiting bucket for the Twitch join rate limits
// https://dev.twitch.tv/docs/irc/guide#rate-limits
QObjectPtr<RatelimitBucket> joinBucket_;
QTimer reconnectTimer_; QTimer reconnectTimer_;
int falloffCounter_ = 1; int falloffCounter_ = 1;

View file

@ -3,6 +3,7 @@
#include "providers/twitch/PubsubActions.hpp" #include "providers/twitch/PubsubActions.hpp"
#include "providers/twitch/PubsubHelpers.hpp" #include "providers/twitch/PubsubHelpers.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "util/DebugCount.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
@ -23,7 +24,8 @@ namespace chatterino {
static const char *pingPayload = "{\"type\":\"PING\"}"; static const char *pingPayload = "{\"type\":\"PING\"}";
static std::map<QString, QString> sentMessages; static std::map<QString, RequestMessage> sentListens;
static std::map<QString, RequestMessage> sentUnlistens;
namespace detail { namespace detail {
@ -59,8 +61,9 @@ namespace detail {
// This PubSubClient is already at its peak listens // This PubSubClient is already at its peak listens
return false; return false;
} }
this->numListens_ += numRequestedListens; this->numListens_ += numRequestedListens;
DebugCount::increase("PubSub topic pending listens",
numRequestedListens);
for (const auto &topic : message["data"]["topics"].GetArray()) for (const auto &topic : message["data"]["topics"].GetArray())
{ {
@ -68,12 +71,11 @@ namespace detail {
Listener{topic.GetString(), false, false, false}); Listener{topic.GetString(), false, false, false});
} }
auto uuid = generateUuid(); auto nonce = generateUuid();
rj::set(message, "nonce", nonce);
rj::set(message, "nonce", uuid);
QString payload = rj::stringify(message); QString payload = rj::stringify(message);
sentMessages[uuid] = payload; sentListens[nonce] = RequestMessage{payload, numRequestedListens};
this->send(payload.toUtf8()); this->send(payload.toUtf8());
@ -103,14 +105,19 @@ namespace detail {
return; return;
} }
int numRequestedUnlistens = topics.size();
this->numListens_ -= numRequestedUnlistens;
DebugCount::increase("PubSub topic pending unlistens",
numRequestedUnlistens);
auto message = createUnlistenMessage(topics); auto message = createUnlistenMessage(topics);
auto uuid = generateUuid(); auto nonce = generateUuid();
rj::set(message, "nonce", nonce);
rj::set(message, "nonce", generateUuid());
QString payload = rj::stringify(message); QString payload = rj::stringify(message);
sentMessages[uuid] = payload; sentUnlistens[nonce] = RequestMessage{payload, numRequestedUnlistens};
this->send(payload.toUtf8()); this->send(payload.toUtf8());
} }
@ -865,6 +872,13 @@ PubSub::PubSub()
void PubSub::addClient() void PubSub::addClient()
{ {
if (this->addingClient)
{
return;
}
this->addingClient = true;
websocketpp::lib::error_code ec; websocketpp::lib::error_code ec;
auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, 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( this->requests.emplace_back(
std::make_unique<rapidjson::Document>(std::move(msg))); std::make_unique<rapidjson::Document>(std::move(msg)));
DebugCount::increase("PubSub topic backlog");
} }
bool PubSub::tryListen(rapidjson::Document &msg) bool PubSub::tryListen(rapidjson::Document &msg)
@ -1066,7 +1082,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
if (type == "RESPONSE") if (type == "RESPONSE")
{ {
this->handleListenResponse(msg); this->handleResponse(msg);
} }
else if (type == "MESSAGE") else if (type == "MESSAGE")
{ {
@ -1107,6 +1123,9 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
void PubSub::onConnectionOpen(WebsocketHandle hdl) void PubSub::onConnectionOpen(WebsocketHandle hdl)
{ {
DebugCount::increase("PubSub connections");
this->addingClient = false;
auto client = auto client =
std::make_shared<detail::PubSubClient>(this->websocketClient, hdl); std::make_shared<detail::PubSubClient>(this->websocketClient, hdl);
@ -1123,6 +1142,7 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl)
const auto &request = *it; const auto &request = *it;
if (client->listen(*request)) if (client->listen(*request))
{ {
DebugCount::decrease("PubSub topic backlog");
it = this->requests.erase(it); it = this->requests.erase(it);
} }
else else
@ -1130,10 +1150,16 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl)
++it; ++it;
} }
} }
if (!this->requests.empty())
{
this->addClient();
}
} }
void PubSub::onConnectionClose(WebsocketHandle hdl) void PubSub::onConnectionClose(WebsocketHandle hdl)
{ {
DebugCount::decrease("PubSub connections");
auto clientIt = this->clients.find(hdl); auto clientIt = this->clients.find(hdl);
// If this assert goes off, there's something wrong with the connection // 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; return ctx;
} }
void PubSub::handleListenResponse(const rapidjson::Document &msg) void PubSub::handleResponse(const rapidjson::Document &msg)
{ {
QString error; QString error;
if (rj::getSafe(msg, "error", error)) if (!rj::getSafe(msg, "error", error))
{ return;
QString nonce; QString nonce;
rj::getSafe(msg, "nonce", nonce); rj::getSafe(msg, "nonce", nonce);
if (error.isEmpty()) const bool failed = !error.isEmpty();
if (failed)
{ {
qCDebug(chatterinoPubsub) qCDebug(chatterinoPubsub)
<< "Successfully listened to nonce" << nonce; << QString("Error %1 on nonce %2").arg(error, nonce);
// Nothing went wrong }
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; return;
} }
qCDebug(chatterinoPubsub) qCDebug(chatterinoPubsub)
<< "PubSub error:" << error << "on nonce" << nonce; << "Response on unused" << nonce << "client/topic listener mismatch?";
return; }
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);
} }
} }

View file

@ -47,6 +47,11 @@ using WebsocketErrorCode = websocketpp::lib::error_code;
#define MAX_PUBSUB_LISTENS 50 #define MAX_PUBSUB_LISTENS 50
#define MAX_PUBSUB_CONNECTIONS 10 #define MAX_PUBSUB_CONNECTIONS 10
struct RequestMessage {
QString payload;
int topicCount;
};
namespace detail { namespace detail {
struct Listener { struct Listener {
@ -172,6 +177,7 @@ private:
bool isListeningToTopic(const QString &topic); bool isListeningToTopic(const QString &topic);
void addClient(); void addClient();
std::atomic<bool> addingClient{false};
State state = State::Connected; State state = State::Connected;
@ -192,7 +198,9 @@ private:
void onConnectionClose(websocketpp::connection_hdl hdl); void onConnectionClose(websocketpp::connection_hdl hdl);
WebsocketContextPtr onTLSInit(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 handleMessageResponse(const rapidjson::Value &data);
void runThread(); void runThread();

View file

@ -186,23 +186,6 @@ void TwitchAccount::unblockUser(QString userId, std::function<void()> onSuccess,
std::move(onFailure)); std::move(onFailure));
} }
void TwitchAccount::checkFollow(const QString targetUserID,
std::function<void(FollowResult)> 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<const std::set<TwitchUser>> TwitchAccount::accessBlocks() SharedAccessGuard<const std::set<TwitchUser>> TwitchAccount::accessBlocks()
const const
{ {
@ -215,7 +198,7 @@ SharedAccessGuard<const std::set<QString>> TwitchAccount::accessBlockedUserIds()
return this->ignoresUserIds_.accessConst(); return this->ignoresUserIds_.accessConst();
} }
void TwitchAccount::loadEmotes() void TwitchAccount::loadEmotes(std::weak_ptr<Channel> weakChannel)
{ {
qCDebug(chatterinoTwitch) qCDebug(chatterinoTwitch)
<< "Loading Twitch emotes for user" << this->getUserName(); << "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 // 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 // For now, this is necessary as Kraken's equivalent doesn't return all emotes
// See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900 // 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. // Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
this->loadKrakenEmotes(); this->loadKrakenEmotes();
if (auto channel = weakChannel.lock(); channel != nullptr)
{
channel->addMessage(
makeSystemMessage("Twitch subscriber emotes reloaded."));
}
}); });
} }

View file

@ -108,13 +108,10 @@ public:
void unblockUser(QString userId, std::function<void()> onSuccess, void unblockUser(QString userId, std::function<void()> onSuccess,
std::function<void()> onFailure); std::function<void()> onFailure);
void checkFollow(const QString targetUserID,
std::function<void(FollowResult)> onFinished);
SharedAccessGuard<const std::set<QString>> accessBlockedUserIds() const; SharedAccessGuard<const std::set<QString>> accessBlockedUserIds() const;
SharedAccessGuard<const std::set<TwitchUser>> accessBlocks() const; SharedAccessGuard<const std::set<TwitchUser>> accessBlocks() const;
void loadEmotes(); void loadEmotes(std::weak_ptr<Channel> weakChannel = {});
// loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key // 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 // this function makes sure not to load emote sets that have already been loaded
void loadUserstateEmotes(std::function<void()> callback); void loadUserstateEmotes(std::function<void()> callback);

View file

@ -286,7 +286,8 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
if (!reward.isUserInputRequired) if (!reward.isUserInputRequired)
{ {
MessageBuilder builder; MessageBuilder builder;
TwitchMessageBuilder::appendChannelPointRewardMessage(reward, &builder); TwitchMessageBuilder::appendChannelPointRewardMessage(
reward, &builder, this->isMod(), this->isBroadcaster());
this->addMessage(builder.release()); this->addMessage(builder.release());
return; return;
} }
@ -375,8 +376,18 @@ void TwitchChannel::sendMessage(const QString &message)
{ {
if (parsedMessage == this->lastSentMessage_) if (parsedMessage == this->lastSentMessage_)
{ {
auto spaceIndex = parsedMessage.indexOf(' ');
if (spaceIndex == -1)
{
// no spaces found, fall back to old magic character
parsedMessage.append(MAGIC_MESSAGE_SUFFIX); parsedMessage.append(MAGIC_MESSAGE_SUFFIX);
} }
else
{
// replace the space we found in spaceIndex with two spaces
parsedMessage.replace(spaceIndex, 1, " ");
}
}
} }
} }

View file

@ -57,7 +57,7 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
Image::fromUrl(getEmoteLink(id, "3.0"), 0.25), Image::fromUrl(getEmoteLink(id, "3.0"), 0.25),
}, },
Tooltip{name.toHtmlEscaped() + "<br>Twitch Emote"}, Tooltip{name.toHtmlEscaped() + "<br>Twitch Emote"},
Url{QString("https://twitchemotes.com/emotes/%1").arg(id.string)}}); });
} }
return shared; return shared;

View file

@ -197,34 +197,13 @@ void TwitchIrcServer::writeConnectionMessageReceived(
// Below commands enabled through the twitch.tv/commands CAP REQ // Below commands enabled through the twitch.tv/commands CAP REQ
if (command == "USERSTATE") if (command == "USERSTATE")
{ {
// Received USERSTATE upon PRIVMSGing // Received USERSTATE upon sending PRIVMSG messages
handler.handleUserStateMessage(message); handler.handleUserStateMessage(message);
} }
else if (command == "NOTICE") else if (command == "NOTICE")
{ {
static std::unordered_set<std::string> readConnectionOnlyIDs{ // List of expected NOTICE messages on write connection
"host_on", // https://git.kotmisia.pl/Mm2PL/docs/src/branch/master/irc_msg_ids.md#command-results
"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",
};
handler.handleNoticeMessage( handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message)); static_cast<Communi::IrcNoticeMessage *>(message));
} }

View file

@ -114,44 +114,12 @@ TwitchMessageBuilder::TwitchMessageBuilder(
bool TwitchMessageBuilder::isIgnored() const bool TwitchMessageBuilder::isIgnored() const
{ {
if (SharedMessageBuilder::isIgnored()) return isIgnoredMessage({
{ /*.message = */ this->originalMessage_,
return true; /*.twitchUserID = */ this->tags.value("user-id").toString(),
} /*.isMod = */ this->channel->isMod(),
/*.isBroadcaster = */ this->channel->isBroadcaster(),
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<ShowIgnoredUsersMessages>(
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;
} }
void TwitchMessageBuilder::triggerHighlights() void TwitchMessageBuilder::triggerHighlights()
@ -190,7 +158,9 @@ MessagePtr TwitchMessageBuilder::build()
this->args.channelPointRewardId); this->args.channelPointRewardId);
if (reward) 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; 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) if (this->args.isSentWhisper)
{ {
// TODO(pajlada): Re-implement // TODO(pajlada): Re-implement
@ -1249,8 +1231,19 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
} }
void TwitchMessageBuilder::appendChannelPointRewardMessage( 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<TimestampElement>(); builder->emplace<TimestampElement>();
QString redeemed = "Redeemed"; QString redeemed = "Redeemed";
QStringList textList; QStringList textList;

View file

@ -46,7 +46,8 @@ public:
MessagePtr build() override; MessagePtr build() override;
static void appendChannelPointRewardMessage( 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 // Message in the /live chat for channel going live
static void liveMessage(const QString &channelName, static void liveMessage(const QString &channelName,

View file

@ -142,25 +142,6 @@ void Helix::getUserFollowers(
std::move(failureCallback)); std::move(failureCallback));
} }
void Helix::getUserFollow(
QString userId, QString targetId,
ResultCallback<bool, HelixUsersFollowsRecord> 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( void Helix::fetchStreams(
QStringList userIds, QStringList userLogins, QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback, ResultCallback<std::vector<HelixStream>> successCallback,
@ -354,50 +335,6 @@ void Helix::getGameById(QString gameId,
failureCallback); failureCallback);
} }
void Helix::followUser(QString userId, QString targetId,
std::function<void()> 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<void()> 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, void Helix::createClip(QString channelId,
ResultCallback<HelixClip> successCallback, ResultCallback<HelixClip> successCallback,
std::function<void(HelixClipError)> failureCallback, std::function<void(HelixClipError)> failureCallback,
@ -775,7 +712,7 @@ void Helix::getEmoteSetData(QString emoteSetId,
QJsonObject root = result.parseJson(); QJsonObject root = result.parseJson();
auto data = root.value("data"); auto data = root.value("data");
if (!data.isArray()) if (!data.isArray() || data.toArray().isEmpty())
{ {
failureCallback(); failureCallback();
return Failure; return Failure;

View file

@ -341,11 +341,6 @@ public:
ResultCallback<HelixUsersFollowsResponse> successCallback, ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback); HelixFailureCallback failureCallback);
void getUserFollow(
QString userId, QString targetId,
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#get-streams // https://dev.twitch.tv/docs/api/reference#get-streams
void fetchStreams(QStringList userIds, QStringList userLogins, void fetchStreams(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback, ResultCallback<std::vector<HelixStream>> successCallback,
@ -372,16 +367,6 @@ public:
void getGameById(QString gameId, ResultCallback<HelixGame> successCallback, void getGameById(QString gameId, ResultCallback<HelixGame> successCallback,
HelixFailureCallback failureCallback); HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#create-user-follows
void followUser(QString userId, QString targetId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#delete-user-follows
void unfollowUser(QString userId, QString targetlId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#create-clip // https://dev.twitch.tv/docs/api/reference#create-clip
void createClip(QString channelId, void createClip(QString channelId,
ResultCallback<HelixClip> successCallback, ResultCallback<HelixClip> successCallback,

View file

@ -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 - `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 - `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 ### Create Clip
URL: https://dev.twitch.tv/docs/api/reference#create-clip URL: https://dev.twitch.tv/docs/api/reference#create-clip

View file

@ -191,7 +191,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
const QJsonObject &root) const QJsonObject &root)
{ {
auto app = getApp(); auto app = getApp();
QString action = root.value("action").toString(); QString action = root.value("action").toString();
if (action.isNull()) if (action.isNull())
@ -211,13 +210,20 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
AttachedWindow::GetArgs args; AttachedWindow::GetArgs args;
args.winId = root.value("winId").toString(); args.winId = root.value("winId").toString();
args.yOffset = root.value("yOffset").toInt(-1); 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; args.fullscreen = attachFullscreen;
qCDebug(chatterinoNativeMessage) 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()) if (_type.isNull() || args.winId.isNull())
{ {

View file

@ -23,6 +23,7 @@ ConcurrentSettings::ConcurrentSettings()
, ignoredMessages(*new SignalVector<IgnorePhrase>()) , ignoredMessages(*new SignalVector<IgnorePhrase>())
, mutedChannels(*new SignalVector<QString>()) , mutedChannels(*new SignalVector<QString>())
, filterRecords(*new SignalVector<FilterRecordPtr>()) , filterRecords(*new SignalVector<FilterRecordPtr>())
, nicknames(*new SignalVector<Nickname>())
, moderationActions(*new SignalVector<ModerationAction>) , moderationActions(*new SignalVector<ModerationAction>)
{ {
persist(this->highlightedMessages, "/highlighting/highlights"); persist(this->highlightedMessages, "/highlighting/highlights");
@ -32,6 +33,7 @@ ConcurrentSettings::ConcurrentSettings()
persist(this->ignoredMessages, "/ignore/phrases"); persist(this->ignoredMessages, "/ignore/phrases");
persist(this->mutedChannels, "/pings/muted"); persist(this->mutedChannels, "/pings/muted");
persist(this->filterRecords, "/filtering/filters"); persist(this->filterRecords, "/filtering/filters");
persist(this->nicknames, "/nicknames");
// tagged users? // tagged users?
persist(this->moderationActions, "/moderation/actions"); persist(this->moderationActions, "/moderation/actions");
} }

View file

@ -10,6 +10,7 @@
#include "controllers/highlights/HighlightBadge.hpp" #include "controllers/highlights/HighlightBadge.hpp"
#include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/highlights/HighlightPhrase.hpp"
#include "controllers/moderationactions/ModerationAction.hpp" #include "controllers/moderationactions/ModerationAction.hpp"
#include "controllers/nicknames/Nickname.hpp"
#include "singletons/Toasts.hpp" #include "singletons/Toasts.hpp"
#include "util/StreamerMode.hpp" #include "util/StreamerMode.hpp"
#include "widgets/Notebook.hpp" #include "widgets/Notebook.hpp"
@ -23,6 +24,7 @@ class HighlightBlacklistUser;
class IgnorePhrase; class IgnorePhrase;
class TaggedUser; class TaggedUser;
class FilterRecord; class FilterRecord;
class Nickname;
/// Settings which are availlable for reading on all threads. /// Settings which are availlable for reading on all threads.
class ConcurrentSettings class ConcurrentSettings
@ -37,6 +39,7 @@ public:
SignalVector<IgnorePhrase> &ignoredMessages; SignalVector<IgnorePhrase> &ignoredMessages;
SignalVector<QString> &mutedChannels; SignalVector<QString> &mutedChannels;
SignalVector<FilterRecordPtr> &filterRecords; SignalVector<FilterRecordPtr> &filterRecords;
SignalVector<Nickname> &nicknames;
//SignalVector<TaggedUser> &taggedUsers; //SignalVector<TaggedUser> &taggedUsers;
SignalVector<ModerationAction> &moderationActions; SignalVector<ModerationAction> &moderationActions;

View file

@ -1,12 +1,15 @@
#define LOOKUP_COLOR_COUNT 360
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "singletons/Resources.hpp"
#include <QColor> #include <QColor>
#include <cmath> #include <cmath>
#define LOOKUP_COLOR_COUNT 360
namespace chatterino { namespace chatterino {
Theme::Theme() Theme::Theme()
@ -80,6 +83,16 @@ void Theme::actuallyUpdate(double hue, double multiplier)
this->splits.background = getColor(0, sat, 1); this->splits.background = getColor(0, sat, 1);
this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreview = QColor(0, 148, 255, 0x30);
this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); 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) void Theme::normalizeColor(QColor &color)

View file

@ -48,6 +48,10 @@ public:
} input; } input;
} splits; } splits;
struct {
QPixmap copy;
} buttons;
void normalizeColor(QColor &color); void normalizeColor(QColor &color);
private: private:

View file

@ -27,6 +27,20 @@ public:
reinterpret_cast<int64_t &>(it.value())++; reinterpret_cast<int64_t &>(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<int64_t &>(it.value()) += amount;
}
}
static void decrease(const QString &name) static void decrease(const QString &name)
{ {
@ -42,6 +56,20 @@ public:
reinterpret_cast<int64_t &>(it.value())--; reinterpret_cast<int64_t &>(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<int64_t &>(it.value()) -= amount;
}
}
static QString getDebugText() static QString getDebugText()
{ {

View file

@ -0,0 +1,45 @@
#include "RatelimitBucket.hpp"
#include <QTimer>
namespace chatterino {
RatelimitBucket::RatelimitBucket(int budget, int cooldown,
std::function<void(QString)> 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

View file

@ -0,0 +1,40 @@
#pragma once
#include <QList>
#include <QObject>
#include <QString>
namespace chatterino {
class RatelimitBucket : public QObject
{
public:
RatelimitBucket(int budget, int cooldown,
std::function<void(QString)> 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<void(QString)> callback_;
QList<QString> 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

View file

@ -93,6 +93,7 @@ AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args)
window->fullscreen_ = args.fullscreen; window->fullscreen_ = args.fullscreen;
window->x_ = args.x; window->x_ = args.x;
window->pixelRatio_ = args.pixelRatio;
if (args.height != -1) if (args.height != -1)
{ {
@ -276,7 +277,16 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr)
// offset // offset
int o = this->fullscreen_ ? 0 : 8; 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), ::MoveWindow(hwnd, int(rect.left + this->x_ * scale + o),
int(rect.bottom - this->height_ * scale - o), int(rect.bottom - this->height_ * scale - o),

View file

@ -17,7 +17,8 @@ public:
struct GetArgs { struct GetArgs {
QString winId; QString winId;
int yOffset = -1; int yOffset = -1;
int x = -1; double x = -1;
double pixelRatio = -1;
int width = -1; int width = -1;
int height = -1; int height = -1;
bool fullscreen = false; bool fullscreen = false;
@ -54,7 +55,8 @@ private:
void *target_; void *target_;
int yOffset_; int yOffset_;
int currentYOffset_; int currentYOffset_;
int x_ = -1; double x_ = -1;
double pixelRatio_ = -1;
int width_ = 360; int width_ = 360;
int height_ = -1; int height_ = -1;
bool fullscreen_ = false; bool fullscreen_ = false;

View file

@ -641,11 +641,7 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
long *result) long *result)
{ {
#ifdef USEWINSDK #ifdef USEWINSDK
# if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1))
MSG *msg = *reinterpret_cast<MSG **>(message);
# else
MSG *msg = reinterpret_cast<MSG *>(message); MSG *msg = reinterpret_cast<MSG *>(message);
# endif
bool returnValue = false; bool returnValue = false;

View file

@ -30,11 +30,7 @@ FramelessEmbedWindow::FramelessEmbedWindow()
bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType, bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType,
void *message, long *result) void *message, long *result)
{ {
# if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1))
MSG *msg = *reinterpret_cast<MSG **>(message);
# else
MSG *msg = reinterpret_cast<MSG *>(message); MSG *msg = reinterpret_cast<MSG *>(message);
# endif
if (msg->message == WM_COPYDATA) if (msg->message == WM_COPYDATA)
{ {

View file

@ -17,6 +17,7 @@
#include "widgets/settingspages/IgnoresPage.hpp" #include "widgets/settingspages/IgnoresPage.hpp"
#include "widgets/settingspages/KeyboardSettingsPage.hpp" #include "widgets/settingspages/KeyboardSettingsPage.hpp"
#include "widgets/settingspages/ModerationPage.hpp" #include "widgets/settingspages/ModerationPage.hpp"
#include "widgets/settingspages/NicknamesPage.hpp"
#include "widgets/settingspages/NotificationPage.hpp" #include "widgets/settingspages/NotificationPage.hpp"
#include <QDialogButtonBox> #include <QDialogButtonBox>
@ -164,6 +165,7 @@ void SettingsDialog::addTabs()
this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg"); this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg");
this->ui_.tabContainer->addSpacing(16); this->ui_.tabContainer->addSpacing(16);
this->addTab([]{return new AccountsPage;}, "Accounts", ":/settings/accounts.svg", SettingsTabId::Accounts); 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->ui_.tabContainer->addSpacing(16);
this->addTab([]{return new CommandPage;}, "Commands", ":/settings/commands.svg"); this->addTab([]{return new CommandPage;}, "Commands", ":/settings/commands.svg");
this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg"); this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg");

View file

@ -13,6 +13,7 @@
#include "providers/twitch/api/Kraken.hpp" #include "providers/twitch/api/Kraken.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/Clipboard.hpp" #include "util/Clipboard.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"
@ -42,7 +43,7 @@ namespace {
{ {
auto label = box.emplace<Label>(); auto label = box.emplace<Label>();
auto button = box.emplace<Button>(); auto button = box.emplace<Button>();
button->setPixmap(getResources().buttons.copyDark); button->setPixmap(getApp()->themes->buttons.copy);
button->setScaleIndependantSize(18, 18); button->setScaleIndependantSize(18, 18);
button->setDim(Button::Dim::Lots); button->setDim(Button::Dim::Lots);
QObject::connect( QObject::connect(
@ -232,7 +233,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
{ {
user->addStretch(1); user->addStretch(1);
user.emplace<QCheckBox>("Follow").assign(&this->ui_.follow);
user.emplace<QCheckBox>("Block").assign(&this->ui_.block); user.emplace<QCheckBox>("Block").assign(&this->ui_.block);
user.emplace<QCheckBox>("Ignore highlights") user.emplace<QCheckBox>("Ignore highlights")
.assign(&this->ui_.ignoreHighlights); .assign(&this->ui_.ignoreHighlights);
@ -402,56 +402,6 @@ void UserInfoPopup::scaleChangedEvent(float /*scale*/)
void UserInfoPopup::installEvents() void UserInfoPopup::installEvents()
{ {
std::weak_ptr<bool> hack = this->hack_;
// follow
QObject::connect(
this->ui_.follow, &QCheckBox::stateChanged,
[this](int newState) mutable {
auto currentUser = getApp()->accounts->twitch.getCurrent();
const auto reenableFollowCheckbox = [this] {
this->ui_.follow->setEnabled(true);
};
if (!this->ui_.follow->isEnabled())
{
// We received a state update while the checkbox was disabled
// This can only happen from the "check current follow state" call
// The state has been updated to properly reflect the users current follow state
reenableFollowCheckbox();
return;
}
switch (newState)
{
case Qt::CheckState::Unchecked: {
this->ui_.follow->setEnabled(false);
getHelix()->unfollowUser(currentUser->getUserId(),
this->userId_,
reenableFollowCheckbox, [] {
//
});
}
break;
case Qt::CheckState::PartiallyChecked: {
// We deliberately ignore this state
}
break;
case Qt::CheckState::Checked: {
this->ui_.follow->setEnabled(false);
getHelix()->followUser(currentUser->getUserId(),
this->userId_,
reenableFollowCheckbox, [] {
//
});
}
break;
}
});
std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false); std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false);
// block // block
@ -615,8 +565,6 @@ void UserInfoPopup::updateLatestMessages()
void UserInfoPopup::updateUserData() void UserInfoPopup::updateUserData()
{ {
this->ui_.follow->setEnabled(false);
std::weak_ptr<bool> hack = this->hack_; std::weak_ptr<bool> hack = this->hack_;
auto currentUser = getApp()->accounts->twitch.getCurrent(); auto currentUser = getApp()->accounts->twitch.getCurrent();
@ -682,19 +630,6 @@ void UserInfoPopup::updateUserData()
// on failure // on failure
}); });
// get follow state
currentUser->checkFollow(user.id, [this, hack](auto result) {
if (!hack.lock())
{
return;
}
if (result != FollowResult_Failed)
{
this->ui_.follow->setChecked(result == FollowResult_Following);
this->ui_.follow->setEnabled(true);
}
});
// get ignore state // get ignore state
bool isIgnoring = false; bool isIgnoring = false;
@ -771,7 +706,6 @@ void UserInfoPopup::updateUserData()
getHelix()->getUserByName(this->userName_, onUserFetched, getHelix()->getUserByName(this->userName_, onUserFetched,
onUserFetchFailed); onUserFetchFailed);
this->ui_.follow->setEnabled(false);
this->ui_.block->setEnabled(false); this->ui_.block->setEnabled(false);
this->ui_.ignoreHighlights->setEnabled(false); this->ui_.ignoreHighlights->setEnabled(false);
} }

View file

@ -59,7 +59,6 @@ private:
Label *followageLabel = nullptr; Label *followageLabel = nullptr;
Label *subageLabel = nullptr; Label *subageLabel = nullptr;
QCheckBox *follow = nullptr;
QCheckBox *block = nullptr; QCheckBox *block = nullptr;
QCheckBox *ignoreHighlights = nullptr; QCheckBox *ignoreHighlights = nullptr;

View file

@ -108,11 +108,7 @@ namespace {
}); });
}; };
if (creatorFlags.has(MessageElementFlag::TwitchEmote)) if (creatorFlags.has(MessageElementFlag::BttvEmote))
{
addPageLink("TwitchEmotes");
}
else if (creatorFlags.has(MessageElementFlag::BttvEmote))
{ {
addPageLink("BTTV"); addPageLink("BTTV");
} }
@ -754,7 +750,7 @@ bool ChannelView::shouldIncludeMessage(const MessagePtr &m) const
m->loginName, Qt::CaseInsensitive) == 0) m->loginName, Qt::CaseInsensitive) == 0)
return true; return true;
return this->channelFilters_->filter(m); return this->channelFilters_->filter(m, this->channel_);
} }
return true; return true;

View file

@ -44,7 +44,7 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
} }
if (accept && filterSet) if (accept && filterSet)
accept = filterSet->filter(message); accept = filterSet->filter(message, channel);
// If all predicates match, add the message to the channel // If all predicates match, add the message to the channel
if (accept) if (accept)

View file

@ -17,6 +17,11 @@
#define PIXMAP_WIDTH 500 #define PIXMAP_WIDTH 500
#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com"
#define LINK_DONATE "https://streamelements.com/fourtf/tip"
#define LINK_CHATTERINO_FEATURES "https://chatterino.com/#features"
#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z"
namespace chatterino { namespace chatterino {
AboutPage::AboutPage() AboutPage::AboutPage()
@ -76,6 +81,7 @@ AboutPage::AboutPage()
// } // }
}*/ }*/
// Version
auto versionInfo = layout.emplace<QGroupBox>("Version"); auto versionInfo = layout.emplace<QGroupBox>("Version");
{ {
auto version = Version::instance(); auto version = Version::instance();
@ -96,6 +102,20 @@ AboutPage::AboutPage()
Qt::LinksAccessibleByMouse); Qt::LinksAccessibleByMouse);
} }
// About Chatterino
auto aboutChatterino = layout.emplace<QGroupBox>("About Chatterino...");
{
auto l = aboutChatterino.emplace<QVBoxLayout>();
// clang-format off
l.emplace<QLabel>("Chatterino Wiki can be found <a href=\"" LINK_CHATTERINO_WIKI "\">here</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Support <a href=\"" LINK_DONATE "\">Chatterino</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("All about Chatterino's <a href=\"" LINK_CHATTERINO_FEATURES "\">features</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Join the official Chatterino <a href=\"" LINK_CHATTERINO_DISCORD "\">Discord</a>")->setOpenExternalLinks(true);
// clang-format on
}
// Licenses
auto licenses = auto licenses =
layout.emplace<QGroupBox>("Open source software used..."); layout.emplace<QGroupBox>("Open source software used...");
{ {
@ -129,6 +149,7 @@ AboutPage::AboutPage()
":/licenses/lrucache.txt"); ":/licenses/lrucache.txt");
} }
// Attributions
auto attributions = layout.emplace<QGroupBox>("Attributions..."); auto attributions = layout.emplace<QGroupBox>("Attributions...");
{ {
auto l = attributions.emplace<QVBoxLayout>(); auto l = attributions.emplace<QVBoxLayout>();
@ -140,7 +161,6 @@ AboutPage::AboutPage()
l.emplace<QLabel>("Google emojis provided by <a href=\"https://google.com\">Google</a>")->setOpenExternalLinks(true); l.emplace<QLabel>("Google emojis provided by <a href=\"https://google.com\">Google</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a>" l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a>"
"(<a href=\"https://github.com/iamcal/emoji-data/blob/master/LICENSE\">show license</a>)")->setOpenExternalLinks(true); "(<a href=\"https://github.com/iamcal/emoji-data/blob/master/LICENSE\">show license</a>)")->setOpenExternalLinks(true);
l.emplace<QLabel>("Twitch emote data provided by <a href=\"https://twitchemotes.com/\">twitchemotes.com</a> through the <a href=\"https://github.com/Chatterino/api\">Chatterino API</a>")->setOpenExternalLinks(true);
// clang-format on // clang-format on
} }

View file

@ -195,7 +195,7 @@ HighlightingPage::HighlightingPage()
} }
getSettings()->highlightedBadges.append(HighlightBadge{ getSettings()->highlightedBadges.append(HighlightBadge{
s->badgeName(), s->displayName(), false, false, "", s->badgeName(), s->displayName(), false, false, "",
ColorProvider::instance().color( *ColorProvider::instance().color(
ColorType::SelfHighlight)}); ColorType::SelfHighlight)});
} }
}); });

View file

@ -160,7 +160,7 @@ ModerationPage::ModerationPage()
"Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>" "Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>"
"Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\", \"/w someusername !report {user} was bad in channel {channel}\" or any other custom text commands.<br>" "Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\", \"/w someusername !report {user} was bad in channel {channel}\" or any other custom text commands.<br>"
"For deleting messages use /delete {msg-id}.<br><br>" "For deleting messages use /delete {msg-id}.<br><br>"
"More information can be found <a href='http://wiki.chatterino.com/Moderation/#moderation-mode'>here</a>."); "More information can be found <a href='https://wiki.chatterino.com/Moderation/#moderation-mode'>here</a>.");
label->setOpenExternalLinks(true); label->setOpenExternalLinks(true);
label->setWordWrap(true); label->setWordWrap(true);
label->setStyleSheet("color: #bbb"); label->setStyleSheet("color: #bbb");

View file

@ -0,0 +1,48 @@
#include "NicknamesPage.hpp"
#include "controllers/nicknames/NicknamesModel.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Window.hpp"
#include "widgets/helper/EditableModelView.hpp"
#include <QTableView>
#include <QHeaderView>
namespace chatterino {
NicknamesPage::NicknamesPage()
{
LayoutCreator<NicknamesPage> layoutCreator(this);
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
layout.emplace<QLabel>(
"Nicknames do not work with features such as search or user highlights."
"\nWith those features you will still need to use the user's original "
"name.");
EditableModelView *view =
layout
.emplace<EditableModelView>(
(new NicknamesModel(nullptr))
->initialized(&getSettings()->nicknames))
.getElement();
view->setTitles({"Username", "Nickname"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Interactive);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
1, QHeaderView::Stretch);
view->addButtonPressed.connect([] {
getSettings()->nicknames.append(Nickname{"Username", "Nickname"});
});
QTimer::singleShot(1, [view] {
view->getTableView()->resizeColumnsToContents();
view->getTableView()->setColumnWidth(0, 250);
});
}
} // namespace chatterino

View file

@ -0,0 +1,18 @@
#pragma once
#include "widgets/helper/EditableModelView.hpp"
#include "widgets/settingspages/SettingsPage.hpp"
#include <QStringListModel>
class QVBoxLayout;
namespace chatterino {
class NicknamesPage : public SettingsPage
{
public:
NicknamesPage();
};
} // namespace chatterino

View file

@ -624,8 +624,10 @@ void Split::popup()
window.getNotebook().getOrAddSelectedPage())); window.getNotebook().getOrAddSelectedPage()));
split->setChannel(this->getIndirectChannel()); split->setChannel(this->getIndirectChannel());
window.getNotebook().getOrAddSelectedPage()->appendSplit(split); split->setModerationMode(this->getModerationMode());
split->setFilters(this->getFilters());
window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
window.show(); window.show();
} }
@ -868,8 +870,8 @@ void Split::showSearch()
void Split::reloadChannelAndSubscriberEmotes() void Split::reloadChannelAndSubscriberEmotes()
{ {
getApp()->accounts->twitch.getCurrent()->loadEmotes();
auto channel = this->getChannel(); auto channel = this->getChannel();
getApp()->accounts->twitch.getCurrent()->loadEmotes(channel);
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get())) if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
{ {

View file

@ -916,10 +916,6 @@ void SplitHeader::themeChangedEvent()
} }
} }
void SplitHeader::moveSplit()
{
}
void SplitHeader::reloadChannelEmotes() void SplitHeader::reloadChannelEmotes()
{ {
auto channel = this->split_->getChannel(); auto channel = this->split_->getChannel();
@ -933,7 +929,8 @@ void SplitHeader::reloadChannelEmotes()
void SplitHeader::reloadSubscriberEmotes() void SplitHeader::reloadSubscriberEmotes()
{ {
getApp()->accounts->twitch.getCurrent()->loadEmotes(); auto channel = this->split_->getChannel();
getApp()->accounts->twitch.getCurrent()->loadEmotes(channel);
} }
void SplitHeader::reconnect() void SplitHeader::reconnect()

View file

@ -85,7 +85,6 @@ private:
std::vector<pajlada::Signals::ScopedConnection> channelConnections_; std::vector<pajlada::Signals::ScopedConnection> channelConnections_;
public slots: public slots:
void moveSplit();
void reloadChannelEmotes(); void reloadChannelEmotes();
void reloadSubscriberEmotes(); void reloadSubscriberEmotes();
void reconnect(); void reconnect();

View file

@ -134,14 +134,18 @@ void SplitInput::themeChangedEvent()
QPalette palette, placeholderPalette; QPalette palette, placeholderPalette;
palette.setColor(QPalette::WindowText, this->theme->splits.input.text); palette.setColor(QPalette::WindowText, this->theme->splits.input.text);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
placeholderPalette.setColor( placeholderPalette.setColor(
QPalette::PlaceholderText, QPalette::PlaceholderText,
this->theme->messages.textColors.chatPlaceholder); this->theme->messages.textColors.chatPlaceholder);
#endif
this->updateEmoteButton(); this->updateEmoteButton();
this->ui_.textEditLength->setPalette(palette); this->ui_.textEditLength->setPalette(palette);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
this->ui_.textEdit->setPalette(placeholderPalette); this->ui_.textEdit->setPalette(placeholderPalette);
#endif
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet); this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
this->ui_.hbox->setMargin( this->ui_.hbox->setMargin(

View file

@ -11,6 +11,8 @@ set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/ExponentialBackoff.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ExponentialBackoff.cpp
${CMAKE_CURRENT_LIST_DIR}/src/TwitchAccount.cpp ${CMAKE_CURRENT_LIST_DIR}/src/TwitchAccount.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp
${CMAKE_CURRENT_LIST_DIR}/src/RatelimitBucket.cpp
# Add your new file above this line!
) )
add_executable(${PROJECT_NAME} ${test_SOURCES}) add_executable(${PROJECT_NAME} ${test_SOURCES})

View file

@ -0,0 +1,46 @@
#include "util/RatelimitBucket.hpp"
#include <gtest/gtest.h>
#include <QApplication>
#include <QDebug>
#include <QtConcurrent>
#include <chrono>
#include <thread>
using namespace chatterino;
TEST(RatelimitBucket, BatchTwoParts)
{
const int cooldown = 100;
int n = 0;
auto cb = [&n](QString msg) {
qDebug() << msg;
++n;
};
auto bucket = std::make_unique<RatelimitBucket>(5, cooldown, cb, nullptr);
bucket->send("1");
EXPECT_EQ(n, 1);
bucket->send("2");
EXPECT_EQ(n, 2);
bucket->send("3");
EXPECT_EQ(n, 3);
bucket->send("4");
EXPECT_EQ(n, 4);
bucket->send("5");
EXPECT_EQ(n, 5);
bucket->send("6");
// Rate limit reached, n will not have changed yet. If we wait for the cooldown to run, n should have changed
EXPECT_EQ(n, 5);
QCoreApplication::processEvents();
std::this_thread::sleep_for(std::chrono::milliseconds{cooldown});
QCoreApplication::processEvents();
EXPECT_EQ(n, 6);
}