Merge remote-tracking branch 'origin/master' into zneix/feature/qt6

This commit is contained in:
zneix 2021-08-08 17:14:25 +02:00
commit bbecb5a56b
No known key found for this signature in database
GPG key ID: 911916E0523B22F6
48 changed files with 676 additions and 438 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!

View file

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

View file

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

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 c613600e6a52e6d3166247a05205cf1c755d4868
Subproject commit 95f05478de1623767282d8019ea8f3a4b1178b35

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

@ -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?"},

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
#include <QString>
namespace chatterino {
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
struct IgnoredMessageParameters {
QString message;
QString twitchUserID;
bool isMod;
bool isBroadcaster;
};
bool isIgnoredMessage(IgnoredMessageParameters &&params);
} // namespace chatterino

View file

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

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
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());
}
}

View file

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

View file

@ -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."));
}
});
}

View file

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

View file

@ -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, " ");
}
}
}
}

View file

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

View file

@ -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));
}

View file

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

View file

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

View file

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

View file

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

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

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

@ -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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

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);
}