mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Merge remote-tracking branch 'origin/master' into zneix/feature/qt6
This commit is contained in:
commit
bbecb5a56b
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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!
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -2,18 +2,34 @@
|
|||
|
||||
## Unversioned
|
||||
|
||||
- Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136)
|
||||
- Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143)
|
||||
- Minor: Display a system message when reloading subscription emotes to match BTTV/FFZ behavior (#3135)
|
||||
- Bugfix: Moderation mode and active filters are now preserved when opening a split as a popup. (#3113, #3130)
|
||||
- Bugfix: Fixed a bug that caused all badge highlights to use the same color. (#3132, #3134)
|
||||
- Dev: Renamed CMake's build option `USE_SYSTEM_QT5KEYCHAIN` to `USE_SYSTEM_QTKEYCHAIN`. (#3103)
|
||||
- Dev: Add benchmarks that can be compiled with the `BUILD_BENCHMARKS` CMake flag. Off by default. (#3038)
|
||||
|
||||
## 2.3.4
|
||||
|
||||
- Major: Newly uploaded Twitch emotes are once again present in emote picker and can be autocompleted with Tab as well. (#2992)
|
||||
- Major: Deprecated `/(un)follow` commands and (un)following in the usercards as Twitch has removed this feature for 3rd party applications. (#3076, #3078)
|
||||
- Major: Added the ability to add nicknames for users. (#137, #2981)
|
||||
- Major: Fixed constant disconnections with more than 20 channels by rate-limiting outgoing JOIN messages. (#3112, #3115)
|
||||
- Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999, #3033)
|
||||
- Minor: Received Twitch messages now use the exact same timestamp (obtained from Twitch's server) for every Chatterino user instead of assuming message timestamp on client's side. (#3021)
|
||||
- Minor: Received IRC messages use `time` message tag for timestamp if it's available. (#3021)
|
||||
- Minor: Added informative messages for recent-messages API's errors. (#3029)
|
||||
- Minor: Added section with helpful Chatterino-related links to the About page. (#3068)
|
||||
- Minor: Now uses spaces instead of magic Unicode character for sending duplicate messages (#3081)
|
||||
- Minor: Added `channel.live` filter variable (#3092, #3110)
|
||||
- Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010)
|
||||
- Bugfix: Fixed PubSub not properly trying to resolve pending listens when the pending listens list was larger than 50. (#3037)
|
||||
- Bugfix: Copy buttons in usercard now show properly in light mode (#3057)
|
||||
- Bugfix: Fixed comma appended to username completion when not at the beginning of the message. (#3060)
|
||||
- Bugfix: Fixed bug misplacing chat when zooming on Chrome with Chatterino Native Host extension (#1936)
|
||||
- Bugfix: Channel point redemptions from ignored users are now properly blocked. (#3102)
|
||||
- Dev: Allow building against Qt 5.11 (#3105)
|
||||
- Dev: Ubuntu packages are now available (#2936)
|
||||
- Dev: Disabled update checker on Flatpak. (#3051)
|
||||
- Dev: Add logging for HTTP requests (#2991)
|
||||
|
|
|
@ -7,17 +7,25 @@ list(APPEND CMAKE_MODULE_PATH
|
|||
"${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake"
|
||||
)
|
||||
|
||||
project(chatterino VERSION 2.3.3)
|
||||
project(chatterino VERSION 2.3.4)
|
||||
|
||||
option(BUILD_APP "Build Chatterino" ON)
|
||||
option(BUILD_TESTS "Build the tests for Chatterino" OFF)
|
||||
option(BUILD_BENCHMARKS "Build the benchmarks for Chatterino" OFF)
|
||||
option(USE_SYSTEM_PAJLADA_SETTINGS "Use system pajlada settings library" OFF)
|
||||
option(USE_SYSTEM_LIBCOMMUNI "Use system communi library" OFF)
|
||||
option(USE_SYSTEM_QT6KEYCHAIN "Use system Qt6Keychain library" OFF)
|
||||
option(USE_SYSTEM_QTKEYCHAIN "Use system QtKeychain library" OFF)
|
||||
option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
|
||||
option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF)
|
||||
|
||||
option(USE_CONAN "Use conan" OFF)
|
||||
|
||||
if (BUILD_WITH_QT6)
|
||||
set(MAJOR_QT_VERSION "6")
|
||||
else()
|
||||
set(MAJOR_QT_VERSION "5")
|
||||
endif()
|
||||
|
||||
if (USE_CONAN OR CONAN_EXPORTED)
|
||||
include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake)
|
||||
conan_basic_setup(TARGETS NO_OUTPUT_DIRS)
|
||||
|
@ -31,7 +39,7 @@ endif ()
|
|||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake)
|
||||
|
||||
find_package(Qt6 REQUIRED
|
||||
find_package(Qt${MAJOR_QT_VERSION} REQUIRED
|
||||
COMPONENTS
|
||||
Core
|
||||
Widgets
|
||||
|
@ -72,17 +80,17 @@ endif()
|
|||
# Link QtKeychain statically
|
||||
option(QTKEYCHAIN_STATIC "" ON)
|
||||
|
||||
if (USE_SYSTEM_QT6KEYCHAIN)
|
||||
find_package(Qt6Keychain REQUIRED)
|
||||
if (USE_SYSTEM_QTKEYCHAIN)
|
||||
find_package(Qt${MAJOR_QT_VERSION}Keychain REQUIRED)
|
||||
else()
|
||||
set(QT6KEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain")
|
||||
if (NOT EXISTS "${QT6KEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt")
|
||||
set(QTKEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain")
|
||||
if (NOT EXISTS "${QTKEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Submodules probably not loaded, unable to find lib/qtkeychain/CMakeLists.txt")
|
||||
endif()
|
||||
|
||||
add_subdirectory("${QT6KEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL)
|
||||
if (NOT TARGET qt6keychain)
|
||||
message(FATAL_ERROR "qt6keychain target was not created :@")
|
||||
add_subdirectory("${QTKEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL)
|
||||
if (NOT TARGET qt${MAJOR_QT_VERSION}keychain)
|
||||
message(FATAL_ERROR "qt${MAJOR_QT_VERSION}keychain target was not created :@")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
@ -94,6 +102,11 @@ if (BUILD_TESTS)
|
|||
find_package(GTest REQUIRED)
|
||||
endif ()
|
||||
|
||||
if (BUILD_BENCHMARKS)
|
||||
# Include system benchmark (Google Benchmark)
|
||||
find_package(benchmark REQUIRED)
|
||||
endif ()
|
||||
|
||||
find_package(PajladaSerialize REQUIRED)
|
||||
find_package(PajladaSignals REQUIRED)
|
||||
find_package(LRUCache REQUIRED)
|
||||
|
@ -111,7 +124,7 @@ endif()
|
|||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if (BUILD_TESTS)
|
||||
if (BUILD_TESTS OR BUILD_BENCHMARKS)
|
||||
add_definitions(-DCHATTERINO_TEST)
|
||||
endif ()
|
||||
|
||||
|
@ -122,4 +135,8 @@ if (BUILD_TESTS)
|
|||
add_subdirectory(tests)
|
||||
endif ()
|
||||
|
||||
if (BUILD_BENCHMARKS)
|
||||
add_subdirectory(benchmarks)
|
||||
endif ()
|
||||
|
||||
feature_summary(WHAT ALL)
|
||||
|
|
35
benchmarks/.clang-format
Normal file
35
benchmarks/.clang-format
Normal 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
28
benchmarks/CMakeLists.txt
Normal 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
57
benchmarks/src/Emojis.cpp
Normal 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
18
benchmarks/src/main.cpp
Normal 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();
|
||||
}
|
|
@ -14,7 +14,7 @@ CCACHE_BIN = $$system(which ccache)
|
|||
CONFIG+=ccache
|
||||
}
|
||||
|
||||
MINIMUM_REQUIRED_QT_VERSION = 5.12.0
|
||||
MINIMUM_REQUIRED_QT_VERSION = 5.11.0
|
||||
|
||||
!versionAtLeast(QT_VERSION, $$MINIMUM_REQUIRED_QT_VERSION) {
|
||||
error("You're trying to compile with Qt $$QT_VERSION, but minimum required Qt version is $$MINIMUM_REQUIRED_QT_VERSION")
|
||||
|
@ -159,6 +159,7 @@ SOURCES += \
|
|||
src/controllers/highlights/HighlightModel.cpp \
|
||||
src/controllers/highlights/HighlightPhrase.cpp \
|
||||
src/controllers/highlights/UserHighlightModel.cpp \
|
||||
src/controllers/ignores/IgnoreController.cpp \
|
||||
src/controllers/ignores/IgnoreModel.cpp \
|
||||
src/controllers/moderationactions/ModerationAction.cpp \
|
||||
src/controllers/moderationactions/ModerationActionModel.cpp \
|
||||
|
@ -251,6 +252,7 @@ SOURCES += \
|
|||
src/util/LayoutHelper.cpp \
|
||||
src/util/NuulsUploader.cpp \
|
||||
src/util/RapidjsonHelpers.cpp \
|
||||
src/util/RatelimitBucket.cpp \
|
||||
src/util/SplitCommand.cpp \
|
||||
src/util/StreamerMode.cpp \
|
||||
src/util/StreamLink.cpp \
|
||||
|
@ -508,6 +510,7 @@ HEADERS += \
|
|||
src/util/rangealgorithm.hpp \
|
||||
src/util/RapidjsonHelpers.hpp \
|
||||
src/util/RapidJsonSerializeQString.hpp \
|
||||
src/util/RatelimitBucket.hpp \
|
||||
src/util/RemoveScrollAreaBackground.hpp \
|
||||
src/util/SampleCheerMessages.hpp \
|
||||
src/util/SampleLinks.hpp \
|
||||
|
@ -582,6 +585,7 @@ HEADERS += \
|
|||
src/widgets/settingspages/IgnoresPage.hpp \
|
||||
src/widgets/settingspages/KeyboardSettingsPage.hpp \
|
||||
src/widgets/settingspages/ModerationPage.hpp \
|
||||
src/widgets/settingspages/NicknamesPage.hpp \
|
||||
src/widgets/settingspages/NotificationPage.hpp \
|
||||
src/widgets/settingspages/SettingsPage.hpp \
|
||||
src/widgets/splits/ClosedSplits.hpp \
|
||||
|
|
88
docs/test-and-benchmark.md
Normal file
88
docs/test-and-benchmark.md
Normal 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 c613600e6a52e6d3166247a05205cf1c755d4868
|
||||
Subproject commit 95f05478de1623767282d8019ea8f3a4b1178b35
|
|
@ -32,6 +32,6 @@
|
|||
<binary>chatterino</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="2.3.3" date="2021-06-21"/>
|
||||
<release version="2.3.4" date="2021-08-05"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
|
@ -88,6 +88,8 @@ set(SOURCE_FILES
|
|||
controllers/highlights/UserHighlightModel.cpp
|
||||
controllers/highlights/UserHighlightModel.hpp
|
||||
|
||||
controllers/ignores/IgnoreController.cpp
|
||||
controllers/ignores/IgnoreController.hpp
|
||||
controllers/ignores/IgnoreModel.cpp
|
||||
controllers/ignores/IgnoreModel.hpp
|
||||
|
||||
|
@ -290,6 +292,8 @@ set(SOURCE_FILES
|
|||
util/NuulsUploader.hpp
|
||||
util/RapidjsonHelpers.cpp
|
||||
util/RapidjsonHelpers.hpp
|
||||
util/RatelimitBucket.cpp
|
||||
util/RatelimitBucket.hpp
|
||||
util/SplitCommand.cpp
|
||||
util/SplitCommand.hpp
|
||||
util/StreamLink.cpp
|
||||
|
@ -482,16 +486,16 @@ add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES})
|
|||
|
||||
target_link_libraries(${LIBRARY_PROJECT}
|
||||
PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
Qt6::Gui
|
||||
Qt6::Network
|
||||
Qt6::Multimedia
|
||||
Qt6::Svg
|
||||
Qt6::Concurrent
|
||||
Qt${MAJOR_QT_VERSION}::Core
|
||||
Qt${MAJOR_QT_VERSION}::Widgets
|
||||
Qt${MAJOR_QT_VERSION}::Gui
|
||||
Qt${MAJOR_QT_VERSION}::Network
|
||||
Qt${MAJOR_QT_VERSION}::Multimedia
|
||||
Qt${MAJOR_QT_VERSION}::Svg
|
||||
Qt${MAJOR_QT_VERSION}::Concurrent
|
||||
|
||||
LibCommuni::LibCommuni
|
||||
qt6keychain
|
||||
qt${MAJOR_QT_VERSION}keychain
|
||||
Pajlada::Serialize
|
||||
Pajlada::Settings
|
||||
Pajlada::Signals
|
||||
|
@ -520,8 +524,8 @@ if (BUILD_APP)
|
|||
)
|
||||
|
||||
if (MSVC)
|
||||
get_target_property(Qt6_Core_Location Qt6::Core LOCATION)
|
||||
get_filename_component(QT_BIN_DIR ${Qt6_Core_Location} DIRECTORY)
|
||||
get_target_property(Qt_Core_Location Qt${MAJOR_QT_VERSION}::Core LOCATION)
|
||||
get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY)
|
||||
set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $<TARGET_FILE:${EXECUTABLE_PROJECT}> --release --no-compiler-runtime --no-translations --no-opengl-sw)
|
||||
|
||||
install(TARGETS ${EXECUTABLE_PROJECT}
|
||||
|
@ -578,7 +582,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
|
|||
CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\"
|
||||
CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\"
|
||||
)
|
||||
if (USE_SYSTEM_QT6KEYCHAIN)
|
||||
if (USE_SYSTEM_QTKEYCHAIN)
|
||||
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
|
||||
CMAKE_BUILD
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
#define CHATTERINO_VERSION "2.3.3"
|
||||
#define CHATTERINO_VERSION "2.3.4"
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
# define CHATTERINO_OS "win"
|
||||
|
|
|
@ -70,6 +70,32 @@ static const QStringList twitchDefaultCommands{
|
|||
|
||||
static const QStringList whisperCommands{"/w", ".w"};
|
||||
|
||||
// stripUserName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripUserName(QString &userName)
|
||||
{
|
||||
if (userName.startsWith('@'))
|
||||
{
|
||||
userName.remove(0, 1);
|
||||
}
|
||||
if (userName.endsWith(','))
|
||||
{
|
||||
userName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
// stripChannelName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripChannelName(QString &channelName)
|
||||
{
|
||||
if (channelName.startsWith('@') || channelName.startsWith('#'))
|
||||
{
|
||||
channelName.remove(0, 1);
|
||||
}
|
||||
if (channelName.endsWith(','))
|
||||
{
|
||||
channelName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
void sendWhisperMessage(const QString &text)
|
||||
{
|
||||
// (hemirt) pajlada: "we should not be sending whispers through jtv, but
|
||||
|
@ -373,6 +399,22 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/follow", [](const auto &words, auto channel) {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"Twitch has removed the ability to follow users through "
|
||||
"third-party applications. For more information, see "
|
||||
"https://github.com/Chatterino/chatterino2/issues/3076"));
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/unfollow", [](const auto &words, auto channel) {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"Twitch has removed the ability to unfollow users through "
|
||||
"third-party applications. For more information, see "
|
||||
"https://github.com/Chatterino/chatterino2/issues/3076"));
|
||||
return "";
|
||||
});
|
||||
|
||||
/// Supported commands
|
||||
|
||||
this->registerCommand(
|
||||
|
@ -407,90 +449,6 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
|
||||
this->registerCommand("/unblock", unblockLambda);
|
||||
|
||||
this->registerCommand("/follow", [](const auto &words, auto channel) {
|
||||
if (words.size() < 2)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage("Usage: /follow [user]"));
|
||||
return "";
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("You must be logged in to follow someone!"));
|
||||
return "";
|
||||
}
|
||||
|
||||
auto target = words.at(1);
|
||||
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[currentUser, channel, target](const auto &targetUser) {
|
||||
getHelix()->followUser(
|
||||
currentUser->getUserId(), targetUser.id,
|
||||
[channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully followed " + target));
|
||||
},
|
||||
[channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("User %1 could not be followed, an unknown "
|
||||
"error occurred!")
|
||||
.arg(target)));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QString("User %1 could not be followed, "
|
||||
"no user with that name found!")
|
||||
.arg(target)));
|
||||
});
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/unfollow", [](const auto &words, auto channel) {
|
||||
if (words.size() < 2)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage("Usage: /unfollow [user]"));
|
||||
return "";
|
||||
}
|
||||
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
if (currentUser->isAnon())
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You must be logged in to unfollow someone!"));
|
||||
return "";
|
||||
}
|
||||
|
||||
auto target = words.at(1);
|
||||
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[currentUser, channel, target](const auto &targetUser) {
|
||||
getHelix()->unfollowUser(
|
||||
currentUser->getUserId(), targetUser.id,
|
||||
[channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"You successfully unfollowed " + target));
|
||||
},
|
||||
[channel, target]() {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"An error occurred while unfollowing " + target));
|
||||
});
|
||||
},
|
||||
[channel, target] {
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("User %1 could not be followed!").arg(target)));
|
||||
});
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/user", [](const auto &words, auto channel) {
|
||||
if (words.size() < 2)
|
||||
{
|
||||
|
@ -498,16 +456,17 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
makeSystemMessage("Usage /user [user] (channel)"));
|
||||
return "";
|
||||
}
|
||||
QString userName = words[1];
|
||||
stripUserName(userName);
|
||||
|
||||
QString channelName = channel->getName();
|
||||
|
||||
if (words.size() > 2)
|
||||
{
|
||||
channelName = words[2];
|
||||
if (channelName[0] == '#')
|
||||
{
|
||||
channelName.remove(0, 1);
|
||||
}
|
||||
stripChannelName(channelName);
|
||||
}
|
||||
openTwitchUsercard(channelName, words[1]);
|
||||
openTwitchUsercard(channelName, userName);
|
||||
|
||||
return "";
|
||||
});
|
||||
|
@ -519,10 +478,12 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
return "";
|
||||
}
|
||||
|
||||
QString userName = words[1];
|
||||
stripUserName(userName);
|
||||
auto *userPopup = new UserInfoPopup(
|
||||
getSettings()->autoCloseUserPopup,
|
||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())));
|
||||
userPopup->setData(words[1], channel);
|
||||
userPopup->setData(userName, channel);
|
||||
userPopup->move(QCursor::pos());
|
||||
userPopup->show();
|
||||
return "";
|
||||
|
|
|
@ -60,11 +60,6 @@ public:
|
|||
return this->parser_->valid();
|
||||
}
|
||||
|
||||
bool filter(const MessagePtr &message) const
|
||||
{
|
||||
return this->parser_->execute(message);
|
||||
}
|
||||
|
||||
bool filter(const filterparser::ContextMap &context) const
|
||||
{
|
||||
return this->parser_->execute(context);
|
||||
|
|
|
@ -36,12 +36,13 @@ public:
|
|||
this->listener_.disconnect();
|
||||
}
|
||||
|
||||
bool filter(const MessagePtr &m) const
|
||||
bool filter(const MessagePtr &m, ChannelPtr channel) const
|
||||
{
|
||||
if (this->filters_.size() == 0)
|
||||
return true;
|
||||
|
||||
filterparser::ContextMap context = filterparser::buildContextMap(m);
|
||||
filterparser::ContextMap context =
|
||||
filterparser::buildContextMap(m, channel.get());
|
||||
for (const auto &f : this->filters_.values())
|
||||
{
|
||||
if (!f->valid() || !f->filter(context))
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
#include "FilterParser.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m)
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||
{
|
||||
auto watchingChannel =
|
||||
chatterino::getApp()->twitch.server->watchingChannel.get();
|
||||
|
@ -61,8 +62,7 @@ ContextMap buildContextMap(const MessagePtr &m)
|
|||
subLength = m->badgeInfos.at(subBadge).toInt();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ContextMap vars = {
|
||||
{"author.badges", std::move(badges)},
|
||||
{"author.color", m->usernameColor},
|
||||
{"author.name", m->displayName},
|
||||
|
@ -82,6 +82,19 @@ ContextMap buildContextMap(const MessagePtr &m)
|
|||
{"message.content", m->messageText},
|
||||
{"message.length", m->messageText.length()},
|
||||
};
|
||||
{
|
||||
using namespace chatterino;
|
||||
auto *tc = dynamic_cast<TwitchChannel *>(channel);
|
||||
if (channel && !channel->isEmpty() && tc)
|
||||
{
|
||||
vars["channel.live"] = tc->isLive();
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["channel.live"] = false;
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
FilterParser::FilterParser(const QString &text)
|
||||
|
@ -91,12 +104,6 @@ FilterParser::FilterParser(const QString &text)
|
|||
{
|
||||
}
|
||||
|
||||
bool FilterParser::execute(const MessagePtr &message) const
|
||||
{
|
||||
auto context = buildContextMap(message);
|
||||
return this->execute(context);
|
||||
}
|
||||
|
||||
bool FilterParser::execute(const ContextMap &context) const
|
||||
{
|
||||
return this->builtExpression_->execute(context).toBool();
|
||||
|
|
|
@ -3,15 +3,20 @@
|
|||
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m);
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
|
||||
|
||||
class FilterParser
|
||||
{
|
||||
public:
|
||||
FilterParser(const QString &text);
|
||||
bool execute(const MessagePtr &message) const;
|
||||
bool execute(const ContextMap &context) const;
|
||||
bool valid() const;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
|||
{"author.sub_length", "author sub length"},
|
||||
{"channel.name", "channel name"},
|
||||
{"channel.watching", "/watching channel?"},
|
||||
{"channel.live", "Channel live?"},
|
||||
{"flags.highlighted", "highlighted?"},
|
||||
{"flags.points_redeemed", "redeemed points?"},
|
||||
{"flags.sub_message", "sub/resub message?"},
|
||||
|
|
63
src/controllers/ignores/IgnoreController.cpp
Normal file
63
src/controllers/ignores/IgnoreController.cpp
Normal 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 &¶ms)
|
||||
{
|
||||
if (!params.message.isEmpty())
|
||||
{
|
||||
// TODO(pajlada): Do we need to check if the phrase is valid first?
|
||||
auto phrases = getCSettings().ignoredMessages.readOnly();
|
||||
for (const auto &phrase : *phrases)
|
||||
{
|
||||
if (phrase.isBlock() && phrase.isMatch(params.message))
|
||||
{
|
||||
qCDebug(chatterinoMessage)
|
||||
<< "Blocking message because it contains ignored phrase"
|
||||
<< phrase.getPattern();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.twitchUserID.isEmpty() &&
|
||||
getSettings()->enableTwitchBlockedUsers)
|
||||
{
|
||||
auto sourceUserID = params.twitchUserID;
|
||||
|
||||
auto blocks =
|
||||
getApp()->accounts->twitch.getCurrent()->accessBlockedUserIds();
|
||||
|
||||
if (auto it = blocks->find(sourceUserID); it != blocks->end())
|
||||
{
|
||||
switch (static_cast<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
|
|
@ -1,7 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
||||
|
||||
struct IgnoredMessageParameters {
|
||||
QString message;
|
||||
|
||||
QString twitchUserID;
|
||||
bool isMod;
|
||||
bool isBroadcaster;
|
||||
};
|
||||
|
||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
|
@ -103,20 +104,9 @@ void SharedMessageBuilder::parse()
|
|||
|
||||
bool SharedMessageBuilder::isIgnored() const
|
||||
{
|
||||
// TODO(pajlada): Do we need to check if the phrase is valid first?
|
||||
auto phrases = getCSettings().ignoredMessages.readOnly();
|
||||
for (const auto &phrase : *phrases)
|
||||
{
|
||||
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
|
||||
{
|
||||
qCDebug(chatterinoMessage)
|
||||
<< "Blocking message because it contains ignored phrase"
|
||||
<< phrase.getPattern();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return isIgnoredMessage({
|
||||
/*.message = */ this->originalMessage_,
|
||||
});
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::parseUsernameColor()
|
||||
|
|
|
@ -15,6 +15,10 @@ const int RECONNECT_BASE_INTERVAL = 2000;
|
|||
// 60 falloff counter means it will try to reconnect at most every 60*2 seconds
|
||||
const int MAX_FALLOFF_COUNTER = 60;
|
||||
|
||||
// Ratelimits for joinBucket_
|
||||
const int JOIN_RATELIMIT_BUDGET = 18;
|
||||
const int JOIN_RATELIMIT_COOLDOWN = 10500;
|
||||
|
||||
AbstractIrcServer::AbstractIrcServer()
|
||||
{
|
||||
// Initialize the connections
|
||||
|
@ -23,6 +27,17 @@ AbstractIrcServer::AbstractIrcServer()
|
|||
this->writeConnection_->moveToThread(
|
||||
QCoreApplication::instance()->thread());
|
||||
|
||||
// Apply a leaky bucket rate limiting to JOIN messages
|
||||
auto actuallyJoin = [&](QString message) {
|
||||
if (!this->channels.contains(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->readConnection_->sendRaw("JOIN #" + message);
|
||||
};
|
||||
this->joinBucket_.reset(new RatelimitBucket(
|
||||
JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this));
|
||||
|
||||
QObject::connect(this->writeConnection_.get(),
|
||||
&Communi::IrcConnection::messageReceived, this,
|
||||
[this](auto msg) {
|
||||
|
@ -214,11 +229,6 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
|
|||
{
|
||||
this->readConnection_->sendRaw("PART #" + channelName);
|
||||
}
|
||||
|
||||
if (this->writeConnection_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
this->writeConnection_->sendRaw("PART #" + channelName);
|
||||
}
|
||||
}));
|
||||
|
||||
// join irc channel
|
||||
|
@ -229,15 +239,7 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
|
|||
{
|
||||
if (this->readConnection_->isConnected())
|
||||
{
|
||||
this->readConnection_->sendRaw("JOIN #" + channelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->writeConnection_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
if (this->readConnection_->isConnected())
|
||||
{
|
||||
this->writeConnection_->sendRaw("JOIN #" + channelName);
|
||||
this->joinBucket_->send(channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +299,7 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
|
|||
{
|
||||
if (auto channel = weak.lock())
|
||||
{
|
||||
connection->sendRaw("JOIN #" + channel->getName());
|
||||
this->joinBucket_->send(channel->getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include "common/Common.hpp"
|
||||
#include "providers/irc/IrcConnection2.hpp"
|
||||
#include "util/RatelimitBucket.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -88,6 +89,10 @@ private:
|
|||
QObjectPtr<IrcConnection> writeConnection_ = 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_;
|
||||
int falloffCounter_ = 1;
|
||||
|
||||
|
|
|
@ -187,23 +187,6 @@ void TwitchAccount::unblockUser(QString userId, std::function<void()> onSuccess,
|
|||
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()
|
||||
const
|
||||
{
|
||||
|
@ -216,7 +199,7 @@ SharedAccessGuard<const std::set<QString>> TwitchAccount::accessBlockedUserIds()
|
|||
return this->ignoresUserIds_.accessConst();
|
||||
}
|
||||
|
||||
void TwitchAccount::loadEmotes()
|
||||
void TwitchAccount::loadEmotes(std::weak_ptr<Channel> weakChannel)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Loading Twitch emotes for user" << this->getUserName();
|
||||
|
@ -238,9 +221,14 @@ void TwitchAccount::loadEmotes()
|
|||
// TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
|
||||
// For now, this is necessary as Kraken's equivalent doesn't return all emotes
|
||||
// See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
|
||||
this->loadUserstateEmotes([=] {
|
||||
this->loadUserstateEmotes([this, weakChannel] {
|
||||
// Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
|
||||
this->loadKrakenEmotes();
|
||||
if (auto channel = weakChannel.lock(); channel != nullptr)
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("Twitch subscriber emotes reloaded."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -108,13 +108,10 @@ public:
|
|||
void unblockUser(QString userId, std::function<void()> onSuccess,
|
||||
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<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
|
||||
// this function makes sure not to load emote sets that have already been loaded
|
||||
void loadUserstateEmotes(std::function<void()> callback);
|
||||
|
|
|
@ -286,7 +286,8 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
|
|||
if (!reward.isUserInputRequired)
|
||||
{
|
||||
MessageBuilder builder;
|
||||
TwitchMessageBuilder::appendChannelPointRewardMessage(reward, &builder);
|
||||
TwitchMessageBuilder::appendChannelPointRewardMessage(
|
||||
reward, &builder, this->isMod(), this->isBroadcaster());
|
||||
this->addMessage(builder.release());
|
||||
return;
|
||||
}
|
||||
|
@ -375,7 +376,17 @@ void TwitchChannel::sendMessage(const QString &message)
|
|||
{
|
||||
if (parsedMessage == this->lastSentMessage_)
|
||||
{
|
||||
parsedMessage.append(MAGIC_MESSAGE_SUFFIX);
|
||||
auto spaceIndex = parsedMessage.indexOf(' ');
|
||||
if (spaceIndex == -1)
|
||||
{
|
||||
// no spaces found, fall back to old magic character
|
||||
parsedMessage.append(MAGIC_MESSAGE_SUFFIX);
|
||||
}
|
||||
else
|
||||
{
|
||||
// replace the space we found in spaceIndex with two spaces
|
||||
parsedMessage.replace(spaceIndex, 1, " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
|
|||
Image::fromUrl(getEmoteLink(id, "3.0"), 0.25),
|
||||
},
|
||||
Tooltip{name.toHtmlEscaped() + "<br>Twitch Emote"},
|
||||
Url{QString("https://twitchemotes.com/emotes/%1").arg(id.string)}});
|
||||
});
|
||||
}
|
||||
|
||||
return shared;
|
||||
|
|
|
@ -197,34 +197,13 @@ void TwitchIrcServer::writeConnectionMessageReceived(
|
|||
// Below commands enabled through the twitch.tv/commands CAP REQ
|
||||
if (command == "USERSTATE")
|
||||
{
|
||||
// Received USERSTATE upon PRIVMSGing
|
||||
// Received USERSTATE upon sending PRIVMSG messages
|
||||
handler.handleUserStateMessage(message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
static std::unordered_set<std::string> readConnectionOnlyIDs{
|
||||
"host_on",
|
||||
"host_off",
|
||||
"host_target_went_offline",
|
||||
"emote_only_on",
|
||||
"emote_only_off",
|
||||
"slow_on",
|
||||
"slow_off",
|
||||
"subs_on",
|
||||
"subs_off",
|
||||
"r9k_on",
|
||||
"r9k_off",
|
||||
|
||||
// Display for user who times someone out. This implies you're a
|
||||
// moderator, at which point you will be connected to PubSub and receive
|
||||
// a better message from there.
|
||||
"timeout_success",
|
||||
"ban_success",
|
||||
|
||||
// Channel suspended notices
|
||||
"msg_channel_suspended",
|
||||
};
|
||||
|
||||
// List of expected NOTICE messages on write connection
|
||||
// https://git.kotmisia.pl/Mm2PL/docs/src/branch/master/irc_msg_ids.md#command-results
|
||||
handler.handleNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
|
|
|
@ -113,44 +113,12 @@ TwitchMessageBuilder::TwitchMessageBuilder(
|
|||
|
||||
bool TwitchMessageBuilder::isIgnored() const
|
||||
{
|
||||
if (SharedMessageBuilder::isIgnored())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto app = getApp();
|
||||
|
||||
if (getSettings()->enableTwitchBlockedUsers &&
|
||||
this->tags.contains("user-id"))
|
||||
{
|
||||
auto sourceUserID = this->tags.value("user-id").toString();
|
||||
|
||||
auto blocks =
|
||||
app->accounts->twitch.getCurrent()->accessBlockedUserIds();
|
||||
|
||||
if (auto it = blocks->find(sourceUserID); it != blocks->end())
|
||||
{
|
||||
switch (static_cast<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;
|
||||
return isIgnoredMessage({
|
||||
/*.message = */ this->originalMessage_,
|
||||
/*.twitchUserID = */ this->tags.value("user-id").toString(),
|
||||
/*.isMod = */ this->channel->isMod(),
|
||||
/*.isBroadcaster = */ this->channel->isBroadcaster(),
|
||||
});
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::triggerHighlights()
|
||||
|
@ -189,7 +157,9 @@ MessagePtr TwitchMessageBuilder::build()
|
|||
this->args.channelPointRewardId);
|
||||
if (reward)
|
||||
{
|
||||
this->appendChannelPointRewardMessage(reward.get(), this);
|
||||
this->appendChannelPointRewardMessage(
|
||||
reward.get(), this, this->channel->isMod(),
|
||||
this->channel->isBroadcaster());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1261,8 +1231,19 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
|
|||
}
|
||||
|
||||
void TwitchMessageBuilder::appendChannelPointRewardMessage(
|
||||
const ChannelPointReward &reward, MessageBuilder *builder)
|
||||
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
|
||||
bool isBroadcaster)
|
||||
{
|
||||
if (isIgnoredMessage({
|
||||
/*.message = */ "",
|
||||
/*.twitchUserID = */ reward.user.id,
|
||||
/*.isMod = */ isMod,
|
||||
/*.isBroadcaster = */ isBroadcaster,
|
||||
}))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder->emplace<TimestampElement>();
|
||||
QString redeemed = "Redeemed";
|
||||
QStringList textList;
|
||||
|
|
|
@ -46,7 +46,8 @@ public:
|
|||
MessagePtr build() override;
|
||||
|
||||
static void appendChannelPointRewardMessage(
|
||||
const ChannelPointReward &reward, MessageBuilder *builder);
|
||||
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
|
||||
bool isBroadcaster);
|
||||
|
||||
// Message in the /live chat for channel going live
|
||||
static void liveMessage(const QString &channelName,
|
||||
|
|
|
@ -142,25 +142,6 @@ void Helix::getUserFollowers(
|
|||
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(
|
||||
QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixStream>> successCallback,
|
||||
|
@ -354,50 +335,6 @@ void Helix::getGameById(QString gameId,
|
|||
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,
|
||||
ResultCallback<HelixClip> successCallback,
|
||||
std::function<void(HelixClipError)> failureCallback,
|
||||
|
@ -775,7 +712,7 @@ void Helix::getEmoteSetData(QString emoteSetId,
|
|||
QJsonObject root = result.parseJson();
|
||||
auto data = root.value("data");
|
||||
|
||||
if (!data.isArray())
|
||||
if (!data.isArray() || data.toArray().isEmpty())
|
||||
{
|
||||
failureCallback();
|
||||
return Failure;
|
||||
|
|
|
@ -341,11 +341,6 @@ public:
|
|||
ResultCallback<HelixUsersFollowsResponse> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
void getUserFollow(
|
||||
QString userId, QString targetId,
|
||||
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
|
||||
HelixFailureCallback failureCallback);
|
||||
|
||||
// https://dev.twitch.tv/docs/api/reference#get-streams
|
||||
void fetchStreams(QStringList userIds, QStringList userLogins,
|
||||
ResultCallback<std::vector<HelixStream>> successCallback,
|
||||
|
@ -372,16 +367,6 @@ public:
|
|||
void getGameById(QString gameId, ResultCallback<HelixGame> successCallback,
|
||||
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
|
||||
void createClip(QString channelId,
|
||||
ResultCallback<HelixClip> successCallback,
|
||||
|
|
|
@ -47,26 +47,6 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams
|
|||
- `TwitchChannel` to get live status, game, title, and viewer count of a channel
|
||||
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
|
||||
|
||||
### Follow User
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#create-user-follows
|
||||
Requires `user:edit:follows` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp followUser`
|
||||
Used in:
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to follow a user by ticking follow checkbox in usercard
|
||||
- `controllers/commands/CommandController.cpp` in /follow command
|
||||
|
||||
### Unfollow User
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#delete-user-follows
|
||||
Requires `user:edit:follows` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp unfollowUser`
|
||||
Used in:
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to unfollow a user by unticking follow checkbox in usercard
|
||||
- `controllers/commands/CommandController.cpp` in /unfollow command
|
||||
|
||||
### Create Clip
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#create-clip
|
||||
|
|
45
src/util/RatelimitBucket.cpp
Normal file
45
src/util/RatelimitBucket.cpp
Normal 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
|
40
src/util/RatelimitBucket.hpp
Normal file
40
src/util/RatelimitBucket.hpp
Normal 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
|
|
@ -233,7 +233,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
|||
{
|
||||
user->addStretch(1);
|
||||
|
||||
user.emplace<QCheckBox>("Follow").assign(&this->ui_.follow);
|
||||
user.emplace<QCheckBox>("Block").assign(&this->ui_.block);
|
||||
user.emplace<QCheckBox>("Ignore highlights")
|
||||
.assign(&this->ui_.ignoreHighlights);
|
||||
|
@ -403,56 +402,6 @@ void UserInfoPopup::scaleChangedEvent(float /*scale*/)
|
|||
|
||||
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);
|
||||
|
||||
// block
|
||||
|
@ -616,8 +565,6 @@ void UserInfoPopup::updateLatestMessages()
|
|||
|
||||
void UserInfoPopup::updateUserData()
|
||||
{
|
||||
this->ui_.follow->setEnabled(false);
|
||||
|
||||
std::weak_ptr<bool> hack = this->hack_;
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
|
@ -683,19 +630,6 @@ void UserInfoPopup::updateUserData()
|
|||
// 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
|
||||
bool isIgnoring = false;
|
||||
|
||||
|
@ -772,7 +706,6 @@ void UserInfoPopup::updateUserData()
|
|||
getHelix()->getUserByName(this->userName_, onUserFetched,
|
||||
onUserFetchFailed);
|
||||
|
||||
this->ui_.follow->setEnabled(false);
|
||||
this->ui_.block->setEnabled(false);
|
||||
this->ui_.ignoreHighlights->setEnabled(false);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ private:
|
|||
Label *followageLabel = nullptr;
|
||||
Label *subageLabel = nullptr;
|
||||
|
||||
QCheckBox *follow = nullptr;
|
||||
QCheckBox *block = nullptr;
|
||||
QCheckBox *ignoreHighlights = nullptr;
|
||||
|
||||
|
|
|
@ -108,11 +108,7 @@ namespace {
|
|||
});
|
||||
};
|
||||
|
||||
if (creatorFlags.has(MessageElementFlag::TwitchEmote))
|
||||
{
|
||||
addPageLink("TwitchEmotes");
|
||||
}
|
||||
else if (creatorFlags.has(MessageElementFlag::BttvEmote))
|
||||
if (creatorFlags.has(MessageElementFlag::BttvEmote))
|
||||
{
|
||||
addPageLink("BTTV");
|
||||
}
|
||||
|
@ -754,7 +750,7 @@ bool ChannelView::shouldIncludeMessage(const MessagePtr &m) const
|
|||
m->loginName, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
|
||||
return this->channelFilters_->filter(m);
|
||||
return this->channelFilters_->filter(m, this->channel_);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -44,7 +44,7 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
|||
}
|
||||
|
||||
if (accept && filterSet)
|
||||
accept = filterSet->filter(message);
|
||||
accept = filterSet->filter(message, channel);
|
||||
|
||||
// If all predicates match, add the message to the channel
|
||||
if (accept)
|
||||
|
|
|
@ -162,7 +162,6 @@ AboutPage::AboutPage()
|
|||
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>"
|
||||
"(<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
|
||||
}
|
||||
|
||||
|
|
|
@ -195,7 +195,7 @@ HighlightingPage::HighlightingPage()
|
|||
}
|
||||
getSettings()->highlightedBadges.append(HighlightBadge{
|
||||
s->badgeName(), s->displayName(), false, false, "",
|
||||
ColorProvider::instance().color(
|
||||
*ColorProvider::instance().color(
|
||||
ColorType::SelfHighlight)});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -624,8 +624,10 @@ void Split::popup()
|
|||
window.getNotebook().getOrAddSelectedPage()));
|
||||
|
||||
split->setChannel(this->getIndirectChannel());
|
||||
window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
|
||||
split->setModerationMode(this->getModerationMode());
|
||||
split->setFilters(this->getFilters());
|
||||
|
||||
window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
|
||||
window.show();
|
||||
}
|
||||
|
||||
|
@ -868,8 +870,8 @@ void Split::showSearch()
|
|||
|
||||
void Split::reloadChannelAndSubscriberEmotes()
|
||||
{
|
||||
getApp()->accounts->twitch.getCurrent()->loadEmotes();
|
||||
auto channel = this->getChannel();
|
||||
getApp()->accounts->twitch.getCurrent()->loadEmotes(channel);
|
||||
|
||||
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||
{
|
||||
|
|
|
@ -915,10 +915,6 @@ void SplitHeader::themeChangedEvent()
|
|||
}
|
||||
}
|
||||
|
||||
void SplitHeader::moveSplit()
|
||||
{
|
||||
}
|
||||
|
||||
void SplitHeader::reloadChannelEmotes()
|
||||
{
|
||||
auto channel = this->split_->getChannel();
|
||||
|
@ -932,7 +928,8 @@ void SplitHeader::reloadChannelEmotes()
|
|||
|
||||
void SplitHeader::reloadSubscriberEmotes()
|
||||
{
|
||||
getApp()->accounts->twitch.getCurrent()->loadEmotes();
|
||||
auto channel = this->split_->getChannel();
|
||||
getApp()->accounts->twitch.getCurrent()->loadEmotes(channel);
|
||||
}
|
||||
|
||||
void SplitHeader::reconnect()
|
||||
|
|
|
@ -85,7 +85,6 @@ private:
|
|||
std::vector<pajlada::Signals::ScopedConnection> channelConnections_;
|
||||
|
||||
public slots:
|
||||
void moveSplit();
|
||||
void reloadChannelEmotes();
|
||||
void reloadSubscriberEmotes();
|
||||
void reconnect();
|
||||
|
|
|
@ -134,14 +134,18 @@ void SplitInput::themeChangedEvent()
|
|||
QPalette palette, placeholderPalette;
|
||||
|
||||
palette.setColor(QPalette::WindowText, this->theme->splits.input.text);
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
placeholderPalette.setColor(
|
||||
QPalette::PlaceholderText,
|
||||
this->theme->messages.textColors.chatPlaceholder);
|
||||
#endif
|
||||
|
||||
this->updateEmoteButton();
|
||||
this->ui_.textEditLength->setPalette(palette);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
this->ui_.textEdit->setPalette(placeholderPalette);
|
||||
#endif
|
||||
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||
|
||||
int scale = (this->theme->isLightTheme() ? 4 : 2) * this->scale();
|
||||
|
|
|
@ -11,6 +11,8 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/ExponentialBackoff.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/TwitchAccount.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})
|
||||
|
|
46
tests/src/RatelimitBucket.cpp
Normal file
46
tests/src/RatelimitBucket.cpp
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue