Merge branch 'master' into chat-commands-api

This commit is contained in:
fourtf 2021-08-30 10:40:07 +02:00 committed by GitHub
commit 1c321b640b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1029 additions and 292 deletions

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

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Issue about the Chatterino Browser Extension
url: https://github.com/Chatterino/chatterino-browser-ext/issues
about: Make a suggestion or report a bug about the Chatterino browser extension.
- 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

@ -1,7 +0,0 @@
---
name: Issue about the Chatterino Browser Extension
about: Make a suggestion or report a bug about the Chatterino browser extension.
---
Issues for the extension are tracked here: https://github.com/chatterino/chatterino-browser-ext

View file

@ -13,7 +13,7 @@ jobs:
# Gives an error if there's no change in the changelog (except using label)
- name: Changelog check
uses: dangoslen/changelog-enforcer@v2.2.0
uses: dangoslen/changelog-enforcer@v2.3.1
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'no changelog entry needed, ci, submodules'

View file

@ -33,7 +33,7 @@ Note: This installation will take about 1.5 GB of disk space.
### For our websocket library, we need OpenSSL 1.1
1. Download OpenSSL for windows, version `1.1.1k`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1k.exe)**
1. Download OpenSSL for windows, version `1.1.1l`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1L.exe)**
2. When prompted, install OpenSSL to `C:\local\openssl`
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".

View file

@ -3,6 +3,26 @@
## Unversioned
- Major: Added the ability to autocomplete Fossabot & Streamelements commands. (#3209)
- 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)
- Minor: Allow resub messages to show in `/mentions` tab (#3148)
- Minor: Added a setting to hide similar messages by any user. (#2716)
- Minor: Duplicate spaces now count towards the display message length. (#3002)
- Minor: Commands are now backed up. (#3168)
- Minor: Added the ability to open an entire tab as a popup. (#3082)
- Minor: Added optional parameter to /usercard command for opening a usercard in a different channel context. (#3172)
- Minor: Added regex option to Nicknames. (#3146)
- Minor: Colorizing usernames on IRC, originally made for Mm2PL/dankerino (#3206)
- Bugfix: Fixed colored usernames sometimes not working. (#3170)
- Bugfix: Restored ability to send duplicate `/me` messages. (#3166)
- Bugfix: Notifications for moderators about other moderators deleting messages can now be disabled. (#3121)
- 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)
- Bugfix: Allow starting Streamlink from Chatterino when running as a Flatpak. (#3178)
- Bugfix: Fixed own IRC messages not having metadata and a link to a usercard. (#3203)
- 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
@ -23,6 +43,7 @@
- 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

@ -11,13 +11,21 @@ project(chatterino VERSION 2.3.4)
option(BUILD_APP "Build Chatterino" ON)
option(BUILD_TESTS "Build the tests for Chatterino" OFF)
option(BUILD_BENCHMARKS "Build the benchmarks for Chatterino" OFF)
option(USE_SYSTEM_PAJLADA_SETTINGS "Use system pajlada settings library" OFF)
option(USE_SYSTEM_LIBCOMMUNI "Use system communi library" OFF)
option(USE_SYSTEM_QT5KEYCHAIN "Use system Qt5Keychain library" OFF)
option(USE_SYSTEM_QTKEYCHAIN "Use system QtKeychain library" OFF)
option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF)
option(USE_CONAN "Use conan" OFF)
if (BUILD_WITH_QT6)
set(MAJOR_QT_VERSION "6")
else()
set(MAJOR_QT_VERSION "5")
endif()
if (USE_CONAN OR CONAN_EXPORTED)
include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS NO_OUTPUT_DIRS)
@ -31,7 +39,7 @@ endif ()
include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake)
find_package(Qt5 REQUIRED
find_package(Qt${MAJOR_QT_VERSION} REQUIRED
COMPONENTS
Core
Widgets
@ -72,17 +80,17 @@ endif()
# Link QtKeychain statically
option(QTKEYCHAIN_STATIC "" ON)
if (USE_SYSTEM_QT5KEYCHAIN)
find_package(Qt5Keychain REQUIRED)
if (USE_SYSTEM_QTKEYCHAIN)
find_package(Qt${MAJOR_QT_VERSION}Keychain REQUIRED)
else()
set(QT5KEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain")
if (NOT EXISTS "${QT5KEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt")
set(QTKEYCHAIN_ROOT_LIB_FOLDER "${CMAKE_SOURCE_DIR}/lib/qtkeychain")
if (NOT EXISTS "${QTKEYCHAIN_ROOT_LIB_FOLDER}/CMakeLists.txt")
message(FATAL_ERROR "Submodules probably not loaded, unable to find lib/qtkeychain/CMakeLists.txt")
endif()
add_subdirectory("${QT5KEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL)
if (NOT TARGET qt5keychain)
message(FATAL_ERROR "qt5keychain target was not created :@")
add_subdirectory("${QTKEYCHAIN_ROOT_LIB_FOLDER}" EXCLUDE_FROM_ALL)
if (NOT TARGET qt${MAJOR_QT_VERSION}keychain)
message(FATAL_ERROR "qt${MAJOR_QT_VERSION}keychain target was not created :@")
endif()
endif()
@ -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)

View file

@ -215,3 +215,28 @@ Keep the element on the stack if possible. If you need a pointer or have complex
- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetimes where possible. Objects are destroyed when their parent object is destroyed.
- If you have to explicitly delete an object use `variable->deleteLater()` instead of `delete variable`. This ensures that it will be deleted on the correct thread.
- If an object doesn't have a parent, consider using `std::unique_ptr<Type, DeleteLater>` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope, or the object is destroyed.
## Conventions
#### Usage strings
When informing the user about how a command is supposed to be used, we aim to follow [this standard](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) where possible.
- Square brackets are reserved for `[optional arguments]`.
- Angle brackets are reserved for `<required arguments>`.
- The word _Usage_ should be capitalized and must be followed by a colon.
- If the usage deserves a description, put a dot after all parameters and explain it briefly.
##### Good
- `Usage: /block <user>`
- `Usage: /unblock <user>. Unblocks a user.`
- `Usage: /streamlink <channel>`
- `Usage: /usercard <user> [channel]`
##### Bad
- `Usage /streamlink <channel>` - Missing colon after _Usage_.
- `usage: /streamlink <channel>` - _Usage_ must be capitalized.
- `Usage: /streamlink channel` - The required argument `channel` must be wrapped in angle brackets.
- `Usage: /streamlink <channel>.` - Don't put a dot after usage if it's not followed by a description.

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

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

@ -35,6 +35,7 @@ public:
lru_cache(lru_cache<key_t, value_t> &&other)
: _cache_items_list(std::move(other._cache_items_list))
, _cache_items_map(std::move(other._cache_items_map))
, _max_size(other._max_size)
{
other._cache_items_list.clear();
other._cache_items_map.clear();
@ -44,6 +45,7 @@ public:
{
_cache_items_list = std::move(other._cache_items_list);
_cache_items_map = std::move(other._cache_items_map);
_max_size = other._max_size;
other._cache_items_list.clear();
other._cache_items_map.clear();
return *this;

View file

@ -286,7 +286,7 @@ void Application::initPubsub()
auto chan =
this->twitch.server->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
if (chan->isEmpty() || getSettings()->hideDeletionActions)
{
return;
}

View file

@ -488,16 +488,16 @@ add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES})
target_link_libraries(${LIBRARY_PROJECT}
PUBLIC
Qt5::Core
Qt5::Widgets
Qt5::Gui
Qt5::Network
Qt5::Multimedia
Qt5::Svg
Qt5::Concurrent
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
qt5keychain
qt${MAJOR_QT_VERSION}keychain
Pajlada::Serialize
Pajlada::Settings
Pajlada::Signals
@ -526,8 +526,8 @@ if (BUILD_APP)
)
if (MSVC)
get_target_property(Qt5_Core_Location Qt5::Core LOCATION)
get_filename_component(QT_BIN_DIR ${Qt5_Core_Location} DIRECTORY)
get_target_property(Qt_Core_Location Qt${MAJOR_QT_VERSION}::Core LOCATION)
get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY)
set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $<TARGET_FILE:${EXECUTABLE_PROJECT}> --release --no-compiler-runtime --no-translations --no-opengl-sw)
install(TARGETS ${EXECUTABLE_PROJECT}
@ -584,7 +584,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\"
CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\"
)
if (USE_SYSTEM_QT5KEYCHAIN)
if (USE_SYSTEM_QTKEYCHAIN)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CMAKE_BUILD
)

View file

@ -74,6 +74,12 @@ void ChannelChatters::updateOnlineChatters(
chatters_->updateOnlineChatters(chatters);
}
size_t ChannelChatters::colorsSize() const
{
auto size = this->chatterColors_.access()->size();
return size;
}
const QColor ChannelChatters::getUserColor(const QString &user)
{
const auto chatterColors = this->chatterColors_.access();

View file

@ -25,9 +25,13 @@ public:
void setUserColor(const QString &user, const QColor &color);
void updateOnlineChatters(const std::unordered_set<QString> &chatters);
private:
// colorsSize returns the amount of colors stored in `chatterColors_`
// NOTE: This function is only meant to be used in tests and benchmarks
size_t colorsSize() const;
static constexpr int maxChatterColorCount = 5000;
private:
Channel &channel_;
// maps 2 char prefix to set of names

View file

@ -2,6 +2,8 @@
#include "common/Modes.hpp"
#include <QFileInfo>
#define UGLYMACROHACK1(s) #s
#define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s)
@ -71,4 +73,9 @@ const bool &Version::isSupportedOS() const
return this->isSupportedOS_;
}
bool Version::isFlatpak() const
{
return QFileInfo::exists("/.flatpak-info");
}
} // namespace chatterino

View file

@ -29,6 +29,7 @@ public:
const QString &dateOfBuild() const;
const QString &fullVersion() const;
const bool &isSupportedOS() const;
bool isFlatpak() const;
private:
Version();

View file

@ -110,6 +110,42 @@ void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
descriptor.filters_ = loadFilters(root.value("filters"));
}
TabDescriptor TabDescriptor::loadFromJSON(const QJsonObject &tabObj)
{
TabDescriptor tab;
// Load tab custom title
QJsonValue titleVal = tabObj.value("title");
if (titleVal.isString())
{
tab.customTitle_ = titleVal.toString();
}
// Load tab selected state
tab.selected_ = tabObj.value("selected").toBool(false);
// Load tab "highlightsEnabled" state
tab.highlightsEnabled_ = tabObj.value("highlightsEnabled").toBool(true);
QJsonObject splitRoot = tabObj.value("splits2").toObject();
// Load tab splits
if (!splitRoot.isEmpty())
{
// root type
auto nodeType = splitRoot.value("type").toString();
if (nodeType == "split")
{
tab.rootNode_ = loadNodes<SplitNodeDescriptor>(splitRoot);
}
else if (nodeType == "horizontal" || nodeType == "vertical")
{
tab.rootNode_ = loadNodes<ContainerNodeDescriptor>(splitRoot);
}
}
return tab;
}
WindowLayout WindowLayout::loadFromFile(const QString &path)
{
WindowLayout layout;
@ -117,15 +153,15 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
bool hasSetAMainWindow = false;
// "deserialize"
for (const QJsonValue &window_val : loadWindowArray(path))
for (const QJsonValue &windowVal : loadWindowArray(path))
{
QJsonObject window_obj = window_val.toObject();
QJsonObject windowObj = windowVal.toObject();
WindowDescriptor window;
// Load window type
QString type_val = window_obj.value("type").toString();
auto type = type_val == "main" ? WindowType::Main : WindowType::Popup;
QString typeVal = windowObj.value("type").toString();
auto type = typeVal == "main" ? WindowType::Main : WindowType::Popup;
if (type == WindowType::Main)
{
@ -142,21 +178,21 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
window.type_ = type;
// Load window state
if (window_obj.value("state") == "minimized")
if (windowObj.value("state") == "minimized")
{
window.state_ = WindowDescriptor::State::Minimized;
}
else if (window_obj.value("state") == "maximized")
else if (windowObj.value("state") == "maximized")
{
window.state_ = WindowDescriptor::State::Maximized;
}
// Load window geometry
{
int x = window_obj.value("x").toInt(-1);
int y = window_obj.value("y").toInt(-1);
int width = window_obj.value("width").toInt(-1);
int height = window_obj.value("height").toInt(-1);
int x = windowObj.value("x").toInt(-1);
int y = windowObj.value("y").toInt(-1);
int width = windowObj.value("width").toInt(-1);
int height = windowObj.value("height").toInt(-1);
window.geometry_ = QRect(x, y, width, height);
}
@ -164,23 +200,10 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
bool hasSetASelectedTab = false;
// Load window tabs
QJsonArray tabs = window_obj.value("tabs").toArray();
for (QJsonValue tab_val : tabs)
QJsonArray tabs = windowObj.value("tabs").toArray();
for (QJsonValue tabVal : tabs)
{
TabDescriptor tab;
QJsonObject tab_obj = tab_val.toObject();
// Load tab custom title
QJsonValue title_val = tab_obj.value("title");
if (title_val.isString())
{
tab.customTitle_ = title_val.toString();
}
// Load tab selected state
tab.selected_ = tab_obj.value("selected").toBool(false);
TabDescriptor tab = TabDescriptor::loadFromJSON(tabVal.toObject());
if (tab.selected_)
{
if (hasSetASelectedTab)
@ -192,34 +215,11 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
}
hasSetASelectedTab = true;
}
// Load tab "highlightsEnabled" state
tab.highlightsEnabled_ =
tab_obj.value("highlightsEnabled").toBool(true);
QJsonObject splitRoot = tab_obj.value("splits2").toObject();
// Load tab splits
if (!splitRoot.isEmpty())
{
// root type
auto nodeType = splitRoot.value("type").toString();
if (nodeType == "split")
{
tab.rootNode_ = loadNodes<SplitNodeDescriptor>(splitRoot);
}
else if (nodeType == "horizontal" || nodeType == "vertical")
{
tab.rootNode_ =
loadNodes<ContainerNodeDescriptor>(splitRoot);
}
}
window.tabs_.emplace_back(std::move(tab));
}
// Load emote popup position
QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject();
QJsonObject emote_popup_obj = windowObj.value("emotePopup").toObject();
layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(),
emote_popup_obj.value("y").toInt());

View file

@ -67,6 +67,8 @@ struct ContainerNodeDescriptor {
};
struct TabDescriptor {
static TabDescriptor loadFromJSON(const QJsonObject &root);
QString customTitle_;
bool selected_{false};
bool highlightsEnabled_{true};

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
@ -241,6 +267,8 @@ void CommandController::initialize(Settings &, Paths &paths)
auto path = combinePath(paths.settingsDirectory, "commands.json");
this->sm_ = std::make_shared<pajlada::Settings::SettingManager>();
this->sm_->setPath(path.toStdString());
this->sm_->setBackupEnabled(true);
this->sm_->setBackupSlots(9);
// Delayed initialization of the setting storing all commands
this->commandsSetting_.reset(
@ -268,7 +296,7 @@ void CommandController::initialize(Settings &, Paths &paths)
auto blockLambda = [](const auto &words, auto channel) {
if (words.size() < 2)
{
channel->addMessage(makeSystemMessage("Usage: /block [user]"));
channel->addMessage(makeSystemMessage("Usage: /block <user>"));
return "";
}
@ -313,7 +341,7 @@ void CommandController::initialize(Settings &, Paths &paths)
auto unblockLambda = [](const auto &words, auto channel) {
if (words.size() < 2)
{
channel->addMessage(makeSystemMessage("Usage: /unblock [user]"));
channel->addMessage(makeSystemMessage("Usage: /unblock <user>"));
return "";
}
@ -427,19 +455,20 @@ void CommandController::initialize(Settings &, Paths &paths)
if (words.size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage /user [user] (channel)"));
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 "";
});
@ -447,14 +476,37 @@ void CommandController::initialize(Settings &, Paths &paths)
this->registerCommand("/usercard", [](const auto &words, auto channel) {
if (words.size() < 2)
{
channel->addMessage(makeSystemMessage("Usage /usercard [user]"));
channel->addMessage(
makeSystemMessage("Usage: /usercard <user> [channel]"));
return "";
}
QString userName = words[1];
stripUserName(userName);
if (words.size() > 2)
{
QString channelName = words[2];
stripChannelName(channelName);
ChannelPtr channelTemp =
getApp()->twitch2->getChannelOrEmpty(channelName);
if (channelTemp->isEmpty())
{
channel->addMessage(makeSystemMessage(
"A usercard can only be displayed for a channel that is "
"currently opened in Chatterino."));
return "";
}
channel = channelTemp;
}
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 "";
@ -573,7 +625,7 @@ void CommandController::initialize(Settings &, Paths &paths)
(!channel->isTwitchChannel() || channel->isEmpty()))
{
channel->addMessage(makeSystemMessage(
"Usage: /streamlink [channel]. You can also use the "
"Usage: /streamlink <channel>. You can also use the "
"command without arguments in any Twitch channel to open "
"it in streamlink."));
return "";
@ -594,7 +646,7 @@ void CommandController::initialize(Settings &, Paths &paths)
(!channel->isTwitchChannel() || channel->isEmpty()))
{
channel->addMessage(makeSystemMessage(
"Usage: /popout [channel]. You can also use the command "
"Usage: /popout <channel>. You can also use the command "
"without arguments in any Twitch channel to open its "
"popout chat."));
return "";
@ -621,7 +673,7 @@ void CommandController::initialize(Settings &, Paths &paths)
if (words.size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage: /settitle <stream title>."));
makeSystemMessage("Usage: /settitle <stream title>"));
return "";
}
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
@ -652,7 +704,7 @@ void CommandController::initialize(Settings &, Paths &paths)
if (words.size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage: /setgame <stream game>."));
makeSystemMessage("Usage: /setgame <stream game>"));
return "";
}
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
@ -717,7 +769,7 @@ void CommandController::initialize(Settings &, Paths &paths)
const ChannelPtr channel) {
if (words.size() < 2)
{
channel->addMessage(makeSystemMessage("Usage: /openurl <URL>."));
channel->addMessage(makeSystemMessage("Usage: /openurl <URL>"));
return "";
}

View file

@ -1,7 +1,6 @@
#pragma once
#include "controllers/accounts/AccountController.hpp"
#include "util/RapidJsonSerializeQString.hpp"
#include "util/RapidjsonHelpers.hpp"
@ -15,24 +14,92 @@ namespace chatterino {
class Nickname
{
public:
Nickname(const QString &name, const QString &replace)
Nickname(const QString &name, const QString &replace, const bool isRegex,
const bool isCaseSensitive)
: name_(name)
, replace_(replace)
, isRegex_(isRegex)
, isCaseSensitive_(isCaseSensitive)
, caseSensitivity_(this->isCaseSensitive_ ? Qt::CaseSensitive
: Qt::CaseInsensitive)
{
if (this->isRegex())
{
this->regex_ = QRegularExpression(
name, QRegularExpression::UseUnicodePropertiesOption |
(this->isCaseSensitive()
? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption));
}
}
const QString &name() const
[[nodiscard]] const QString &name() const
{
return this->name_;
}
const QString &replace() const
[[nodiscard]] const QString &replace() const
{
return this->replace_;
}
[[nodiscard]] bool isRegex() const
{
return this->isRegex_;
}
[[nodiscard]] Qt::CaseSensitivity caseSensitivity() const
{
return this->caseSensitivity_;
}
[[nodiscard]] const bool &isCaseSensitive() const
{
return this->isCaseSensitive_;
}
[[nodiscard]] bool match(QString &usernameText) const
{
if (this->isRegex())
{
if (!this->regex_.isValid())
{
return false;
}
if (this->name().isEmpty())
{
return false;
}
auto workingCopy = usernameText;
workingCopy.replace(this->regex_, this->replace());
if (workingCopy != usernameText)
{
usernameText = workingCopy;
return true;
}
}
else
{
auto res =
this->name().compare(usernameText, this->caseSensitivity());
if (res == 0)
{
usernameText = this->replace();
return true;
}
}
return false;
}
private:
QString name_;
QString replace_;
bool isRegex_;
bool isCaseSensitive_;
Qt::CaseSensitivity caseSensitivity_;
QRegularExpression regex_{};
};
} // namespace chatterino
@ -48,6 +115,8 @@ struct Serialize<chatterino::Nickname> {
chatterino::rj::set(ret, "name", value.name(), a);
chatterino::rj::set(ret, "replace", value.replace(), a);
chatterino::rj::set(ret, "isRegex", value.isRegex(), a);
chatterino::rj::set(ret, "isCaseSensitive", value.isCaseSensitive(), a);
return ret;
}
@ -61,16 +130,21 @@ struct Deserialize<chatterino::Nickname> {
if (!value.IsObject())
{
PAJLADA_REPORT_ERROR(error)
return chatterino::Nickname(QString(), QString());
return chatterino::Nickname(QString(), QString(), false, false);
}
QString _name;
QString _replace;
bool _isRegex;
bool _isCaseSensitive;
chatterino::rj::getSafe(value, "name", _name);
chatterino::rj::getSafe(value, "replace", _replace);
chatterino::rj::getSafe(value, "isRegex", _isRegex);
chatterino::rj::getSafe(value, "isCaseSensitive", _isCaseSensitive);
return chatterino::Nickname(_name, _replace);
return chatterino::Nickname(_name, _replace, _isRegex,
_isCaseSensitive);
}
};

View file

@ -8,7 +8,7 @@
namespace chatterino {
NicknamesModel::NicknamesModel(QObject *parent)
: SignalVectorModel<Nickname>(2, parent)
: SignalVectorModel<Nickname>(4, parent)
{
}
@ -17,7 +17,9 @@ Nickname NicknamesModel::getItemFromRow(std::vector<QStandardItem *> &row,
const Nickname &original)
{
return Nickname{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::DisplayRole).toString()};
row[1]->data(Qt::DisplayRole).toString(),
row[2]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool()};
}
// turns a row in the model into a vector item
@ -26,6 +28,8 @@ void NicknamesModel::getRowFromItem(const Nickname &item,
{
setStringItem(row[0], item.name());
setStringItem(row[1], item.replace());
setBoolItem(row[2], item.isRegex());
setBoolItem(row[3], item.isCaseSensitive());
}
} // namespace chatterino

View file

@ -157,10 +157,6 @@ void SharedMessageBuilder::parseHighlights()
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor =
ColorProvider::instance().color(ColorType::Subscription);
// This message was a subscription.
// Don't check for any other highlight phrases.
return;
}
// XXX: Non-common term in SharedMessageBuilder
@ -220,7 +216,10 @@ void SharedMessageBuilder::parseHighlights()
<< "sent a message";
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = userHighlight.getColor();
if (!this->message().flags.has(MessageFlag::Subscription))
{
this->message().highlightColor = userHighlight.getColor();
}
if (userHighlight.showInMentions())
{
@ -289,7 +288,10 @@ void SharedMessageBuilder::parseHighlights()
}
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = highlight.getColor();
if (!this->message().flags.has(MessageFlag::Subscription))
{
this->message().highlightColor = highlight.getColor();
}
if (highlight.showInMentions())
{
@ -344,7 +346,11 @@ void SharedMessageBuilder::parseHighlights()
if (!badgeHighlightSet)
{
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = highlight.getColor();
if (!this->message().flags.has(MessageFlag::Subscription))
{
this->message().highlightColor = highlight.getColor();
}
badgeHighlightSet = true;
}

View file

@ -1,6 +1,7 @@
#include "IrcChannel2.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/irc/IrcCommands.hpp"
#include "providers/irc/IrcServer.hpp"
@ -33,9 +34,14 @@ void IrcChannel::sendMessage(const QString &message)
MessageBuilder builder;
builder.emplace<TimestampElement>();
builder.emplace<TextElement>(this->server()->nick() + ":",
MessageElementFlag::Username);
const auto &nick = this->server()->nick();
builder.emplace<TextElement>(nick + ":", MessageElementFlag::Username)
->setLink({Link::UserInfo, nick});
builder.emplace<TextElement>(message, MessageElementFlag::Text);
builder.message().messageText = message;
builder.message().searchText = nick + ": " + message;
builder.message().loginName = nick;
builder.message().displayName = nick;
this->addMessage(builder.release());
}
}

View file

@ -47,7 +47,7 @@ Outcome invokeIrcCommand(const QString &commandName, const QString &allParams,
}
else if (cmd == "away")
{
sendRaw("AWAY" + params[0] + " :" + paramsAfter(0));
sendRaw("AWAY " + params[0] + " :" + paramsAfter(0));
}
else if (cmd == "knock")
{

View file

@ -11,6 +11,7 @@
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
#include "util/Helpers.hpp"
#include "util/IrcHelpers.hpp"
#include "widgets/Window.hpp"
@ -36,6 +37,7 @@ MessagePtr IrcMessageBuilder::build()
{
// PARSE
this->parse();
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
// PUSH ELEMENTS
this->appendChannelName();

View file

@ -112,11 +112,10 @@ float IrcMessageHandler::similarity(
MessagePtr msg, const LimitedQueueSnapshot<MessagePtr> &messages)
{
float similarityPercent = 0.0f;
int bySameUser = 0;
for (int i = 1; bySameUser < getSettings()->hideSimilarMaxMessagesToCheck;
++i)
int checked = 0;
for (int i = 1; i <= messages.size(); ++i)
{
if (messages.size() < i)
if (checked >= getSettings()->hideSimilarMaxMessagesToCheck)
{
break;
}
@ -126,11 +125,12 @@ float IrcMessageHandler::similarity(
{
break;
}
if (msg->loginName != prevMsg->loginName)
if (getSettings()->hideSimilarBySameUser &&
msg->loginName != prevMsg->loginName)
{
continue;
}
++bySameUser;
++checked;
similarityPercent = std::max(
similarityPercent,
relativeSimilarity(msg->messageText, prevMsg->messageText));
@ -313,12 +313,9 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
const auto highlighted = msg->flags.has(MessageFlag::Highlighted);
const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions);
if (!isSub)
if (highlighted && showInMentions)
{
if (highlighted && showInMentions)
{
server.mentionsChannel->addMessage(msg);
}
server.mentionsChannel->addMessage(msg);
}
chan->addMessage(msg);
@ -813,8 +810,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
if (tags == "bad_delete_message_error" || tags == "usage_delete")
{
channel->addMessage(makeSystemMessage(
"Usage: \"/delete <msg-id>\" - can't take more "
"than one argument"));
"Usage: /delete <msg-id>. Can't take more than one argument"));
}
else if (tags == "host_on" || tags == "host_target_went_offline")
{

View file

@ -198,7 +198,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();
@ -220,9 +220,14 @@ void TwitchAccount::loadEmotes()
// TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
// For now, this is necessary as Kraken's equivalent doesn't return all emotes
// See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
this->loadUserstateEmotes([=] {
this->loadUserstateEmotes([this, weakChannel] {
// Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
this->loadKrakenEmotes();
if (auto channel = weakChannel.lock(); channel != nullptr)
{
channel->addMessage(
makeSystemMessage("Twitch subscriber emotes reloaded."));
}
});
}

View file

@ -111,7 +111,7 @@ public:
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

@ -401,6 +401,16 @@ void TwitchChannel::sendMessage(const QString &message)
if (parsedMessage == this->lastSentMessage_)
{
auto spaceIndex = parsedMessage.indexOf(' ');
// If the message starts with either '/' or '.' Twitch will treat it as a command, omitting
// first space and only rest of the arguments treated as actual message content
// In cases when user sends a message like ". .a b" first character and first space are omitted as well
bool ignoreFirstSpace =
parsedMessage.at(0) == '/' || parsedMessage.at(0) == '.';
if (ignoreFirstSpace)
{
spaceIndex = parsedMessage.indexOf(' ', spaceIndex + 1);
}
if (spaceIndex == -1)
{
// no spaces found, fall back to old magic character

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

@ -184,29 +184,7 @@ MessagePtr TwitchMessageBuilder::build()
this->emplace<TimestampElement>(
calculateMessageTimestamp(this->ircMessage));
bool addModerationElement = true;
if (this->senderIsBroadcaster)
{
addModerationElement = false;
}
else
{
bool hasUserType = this->tags.contains("user-type");
if (hasUserType)
{
QString userType = this->tags.value("user-type").toString();
if (userType == "mod")
{
if (!args.isStaffOrBroadcaster)
{
addModerationElement = false;
}
}
}
}
if (addModerationElement)
if (this->shouldAddModerationElements())
{
this->emplace<TwitchModerationElement>();
}
@ -661,13 +639,11 @@ void TwitchMessageBuilder::appendUsername()
}
auto nicknames = getCSettings().nicknames.readOnly();
auto loginLower = this->message().loginName.toLower();
for (const auto &nickname : *nicknames)
{
if (nickname.name().toLower() == loginLower)
if (nickname.match(usernameText))
{
usernameText = nickname.replace();
break;
}
}
@ -1230,6 +1206,24 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
return Success;
}
bool TwitchMessageBuilder::shouldAddModerationElements() const
{
if (this->senderIsBroadcaster)
{
// You cannot timeout the broadcaster
return false;
}
if (this->tags.value("user-type").toString() == "mod" &&
!this->args.isStaffOrBroadcaster)
{
// You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel
return false;
}
return true;
}
void TwitchMessageBuilder::appendChannelPointRewardMessage(
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
bool isBroadcaster)

View file

@ -90,6 +90,8 @@ private:
void appendFfzBadges();
Outcome tryParseCheermote(const QString &string);
bool shouldAddModerationElements() const;
QString roomID_;
bool hasBits_ = false;
QString bits;

View file

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

@ -392,6 +392,8 @@ public:
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
true};
BoolSetting hideSimilar = {"/similarity/hideSimilar", false};
BoolSetting hideSimilarBySameUser = {"/similarity/hideSimilarBySameUser",
true};
BoolSetting hideSimilarMyself = {"/similarity/hideSimilarMyself", false};
BoolSetting shownSimilarTriggerHighlights = {
"/similarity/shownSimilarTriggerHighlights", false};

View file

@ -232,7 +232,9 @@ void Updates::installUpdates()
void Updates::checkForUpdates()
{
if (!Version::instance().isSupportedOS())
auto version = Version::instance();
if (!version.isSupportedOS())
{
qCDebug(chatterinoUpdate)
<< "Update checking disabled because OS doesn't appear to be one "
@ -241,7 +243,7 @@ void Updates::checkForUpdates()
}
// Disable updates on Flatpak
if (QFileInfo::exists("/.flatpak-info"))
if (version.isFlatpak())
{
return;
}

View file

@ -379,20 +379,20 @@ void WindowManager::save()
QJsonDocument document;
// "serialize"
QJsonArray window_arr;
QJsonArray windowArr;
for (Window *window : this->windows_)
{
QJsonObject window_obj;
QJsonObject windowObj;
// window type
switch (window->getType())
{
case WindowType::Main:
window_obj.insert("type", "main");
windowObj.insert("type", "main");
break;
case WindowType::Popup:
window_obj.insert("type", "popup");
windowObj.insert("type", "popup");
break;
case WindowType::Attached:;
@ -400,68 +400,48 @@ void WindowManager::save()
if (window->isMaximized())
{
window_obj.insert("state", "maximized");
windowObj.insert("state", "maximized");
}
else if (window->isMinimized())
{
window_obj.insert("state", "minimized");
windowObj.insert("state", "minimized");
}
// window geometry
auto rect = window->getBounds();
window_obj.insert("x", rect.x());
window_obj.insert("y", rect.y());
window_obj.insert("width", rect.width());
window_obj.insert("height", rect.height());
windowObj.insert("x", rect.x());
windowObj.insert("y", rect.y());
windowObj.insert("width", rect.width());
windowObj.insert("height", rect.height());
QJsonObject emote_popup_obj;
emote_popup_obj.insert("x", this->emotePopupPos_.x());
emote_popup_obj.insert("y", this->emotePopupPos_.y());
window_obj.insert("emotePopup", emote_popup_obj);
QJsonObject emotePopupObj;
emotePopupObj.insert("x", this->emotePopupPos_.x());
emotePopupObj.insert("y", this->emotePopupPos_.y());
windowObj.insert("emotePopup", emotePopupObj);
// window tabs
QJsonArray tabs_arr;
QJsonArray tabsArr;
for (int tab_i = 0; tab_i < window->getNotebook().getPageCount();
tab_i++)
for (int tabIndex = 0; tabIndex < window->getNotebook().getPageCount();
tabIndex++)
{
QJsonObject tab_obj;
QJsonObject tabObj;
SplitContainer *tab = dynamic_cast<SplitContainer *>(
window->getNotebook().getPageAt(tab_i));
window->getNotebook().getPageAt(tabIndex));
assert(tab != nullptr);
// custom tab title
if (tab->getTab()->hasCustomTitle())
{
tab_obj.insert("title", tab->getTab()->getCustomTitle());
}
// selected
if (window->getNotebook().getSelectedPage() == tab)
{
tab_obj.insert("selected", true);
}
// highlighting on new messages
tab_obj.insert("highlightsEnabled",
tab->getTab()->hasHighlightsEnabled());
// splits
QJsonObject splits;
this->encodeNodeRecursively(tab->getBaseNode(), splits);
tab_obj.insert("splits2", splits);
tabs_arr.append(tab_obj);
bool isSelected = window->getNotebook().getSelectedPage() == tab;
WindowManager::encodeTab(tab, isSelected, tabObj);
tabsArr.append(tabObj);
}
window_obj.insert("tabs", tabs_arr);
window_arr.append(window_obj);
windowObj.insert("tabs", tabsArr);
windowArr.append(windowObj);
}
QJsonObject obj;
obj.insert("windows", window_arr);
obj.insert("windows", windowArr);
document.setObject(obj);
// save file
@ -497,6 +477,32 @@ void WindowManager::queueSave()
this->saveTimer->start(10s);
}
void WindowManager::encodeTab(SplitContainer *tab, bool isSelected,
QJsonObject &obj)
{
// custom tab title
if (tab->getTab()->hasCustomTitle())
{
obj.insert("title", tab->getTab()->getCustomTitle());
}
// selected
if (isSelected)
{
obj.insert("selected", true);
}
// highlighting on new messages
obj.insert("highlightsEnabled", tab->getTab()->hasHighlightsEnabled());
// splits
QJsonObject splits;
WindowManager::encodeNodeRecursively(tab->getBaseNode(), splits);
obj.insert("splits2", splits);
}
void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
{
switch (node->getType())
@ -506,11 +512,12 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
obj.insert("moderationMode", node->getSplit()->getModerationMode());
QJsonObject split;
encodeChannel(node->getSplit()->getIndirectChannel(), split);
WindowManager::encodeChannel(node->getSplit()->getIndirectChannel(),
split);
obj.insert("data", split);
QJsonArray filters;
encodeFilters(node->getSplit(), filters);
WindowManager::encodeFilters(node->getSplit(), filters);
obj.insert("filters", filters);
}
break;
@ -520,14 +527,14 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
? "horizontal"
: "vertical");
QJsonArray items_arr;
QJsonArray itemsArr;
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
{
QJsonObject subObj;
this->encodeNodeRecursively(n.get(), subObj);
items_arr.append(subObj);
WindowManager::encodeNodeRecursively(n.get(), subObj);
itemsArr.append(subObj);
}
obj.insert("items", items_arr);
obj.insert("items", itemsArr);
}
break;
}

View file

@ -30,6 +30,8 @@ public:
WindowManager();
~WindowManager() override;
static void encodeTab(SplitContainer *tab, bool isSelected,
QJsonObject &obj);
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
static void encodeFilters(Split *split, QJsonArray &arr);
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
@ -99,7 +101,8 @@ public:
pajlada::Signals::Signal<SplitContainer *> selectSplitContainer;
private:
void encodeNodeRecursively(SplitContainer::Node *node, QJsonObject &obj);
static void encodeNodeRecursively(SplitContainer::Node *node,
QJsonObject &obj);
// Load window layout from the window-layout.json file
WindowLayout loadWindowLayoutFromFile() const;

View file

@ -10,6 +10,7 @@
#include <QFileInfo>
#include <QProcess>
#include "common/QLogging.hpp"
#include "common/Version.hpp"
#include <functional>
@ -35,18 +36,6 @@ namespace {
#endif
}
QString getStreamlinkProgram()
{
if (getSettings()->streamlinkUseCustomPath)
{
return getSettings()->streamlinkPath + "/" + getBinaryName();
}
else
{
return getBinaryName();
}
}
bool checkStreamlinkPath(const QString &path)
{
QFileInfo fileinfo(path);
@ -83,7 +72,27 @@ namespace {
QProcess *createStreamlinkProcess()
{
auto p = new QProcess;
p->setProgram(getStreamlinkProgram());
const QString path = [] {
if (getSettings()->streamlinkUseCustomPath)
{
return getSettings()->streamlinkPath + "/" + getBinaryName();
}
else
{
return QString{getBinaryName()};
}
}();
if (Version::instance().isFlatpak())
{
p->setProgram("flatpak-spawn");
p->setArguments({"--host", path});
}
else
{
p->setProgram(path);
}
QObject::connect(p, &QProcess::errorOccurred, [=](auto err) {
if (err == QProcess::FailedToStart)
@ -165,7 +174,8 @@ void getStreamQualities(const QString &channelURL,
}
});
p->setArguments({channelURL, "--default-stream=KKona"});
p->setArguments(p->arguments() +
QStringList{channelURL, "--default-stream=KKona"});
p->start();
}
@ -173,7 +183,9 @@ void getStreamQualities(const QString &channelURL,
void openStreamlink(const QString &channelURL, const QString &quality,
QStringList extraArguments)
{
QStringList arguments = extraArguments << channelURL << quality;
auto proc = createStreamlinkProcess();
auto arguments = proc->arguments()
<< extraArguments << channelURL << quality;
// Remove empty arguments before appending additional streamlink options
// as the options might purposely contain empty arguments
@ -182,7 +194,8 @@ void openStreamlink(const QString &channelURL, const QString &quality,
QString additionalOptions = getSettings()->streamlinkOpts.getValue();
arguments << splitCommand(additionalOptions);
bool res = QProcess::startDetached(getStreamlinkProgram(), arguments);
proc->setArguments(std::move(arguments));
bool res = proc->startDetached();
if (!res)
{
@ -200,7 +213,7 @@ void openStreamlinkForChannel(const QString &channel)
if (preferredQuality == "choose")
{
getStreamQualities(channelURL, [=](QStringList qualityOptions) {
QualityPopup::showDialog(channel, qualityOptions);
QualityPopup::showDialog(channelURL, qualityOptions);
});
return;

View file

@ -1,5 +1,7 @@
#include "widgets/BasePopup.hpp"
#include <QAbstractButton>
#include <QDialogButtonBox>
#include <QKeyEvent>
namespace chatterino {
@ -20,4 +22,66 @@ void BasePopup::keyPressEvent(QKeyEvent *e)
BaseWindow::keyPressEvent(e);
}
bool BasePopup::handleEscape(QKeyEvent *e, QDialogButtonBox *buttonBox)
{
assert(buttonBox != nullptr);
if (e->key() == Qt::Key_Escape)
{
auto buttons = buttonBox->buttons();
for (auto *button : buttons)
{
if (auto role = buttonBox->buttonRole(button);
role == QDialogButtonBox::ButtonRole::RejectRole)
{
button->click();
return true;
}
}
}
return false;
}
bool BasePopup::handleEnter(QKeyEvent *e, QDialogButtonBox *buttonBox)
{
assert(buttonBox != nullptr);
if (!e->modifiers() ||
(e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter))
{
switch (e->key())
{
case Qt::Key_Enter:
case Qt::Key_Return: {
auto buttons = buttonBox->buttons();
QAbstractButton *acceptButton = nullptr;
for (auto *button : buttons)
{
if (button->hasFocus())
{
button->click();
return true;
}
if (auto role = buttonBox->buttonRole(button);
role == QDialogButtonBox::ButtonRole::AcceptRole)
{
acceptButton = button;
}
}
if (acceptButton != nullptr)
{
acceptButton->click();
return true;
}
}
break;
}
}
return false;
}
} // namespace chatterino

View file

@ -3,6 +3,8 @@
#include "common/FlagsEnum.hpp"
#include "widgets/BaseWindow.hpp"
class QDialogButtonBox;
namespace chatterino {
class BasePopup : public BaseWindow
@ -13,6 +15,12 @@ public:
protected:
void keyPressEvent(QKeyEvent *e) override;
// handleEscape is a helper function for clicking the "Reject" role button of a button box when the Escape button is pressed
bool handleEscape(QKeyEvent *e, QDialogButtonBox *buttonBox);
// handleEnter is a helper function for clicking the "Accept" role button of a button box when Return or Enter is pressed
bool handleEnter(QKeyEvent *e, QDialogButtonBox *buttonBox);
};
} // namespace chatterino

View file

@ -338,6 +338,14 @@ void Window::addShortcuts()
}
});
createWindowShortcut(this, "CTRL+SHIFT+N", [this] {
if (auto page = dynamic_cast<SplitContainer *>(
this->notebook_->getSelectedPage()))
{
page->popup();
}
});
// Zoom in
{
auto s = new QShortcut(QKeySequence::ZoomIn, this);

View file

@ -7,35 +7,32 @@
namespace chatterino {
QualityPopup::QualityPopup(const QString &_channelName, QStringList options)
QualityPopup::QualityPopup(const QString &channelURL, QStringList options)
: BasePopup({},
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())))
, channelName_(_channelName)
, channelURL_(channelURL)
{
this->ui_.okButton.setText("OK");
this->ui_.cancelButton.setText("Cancel");
this->ui_.selector = new QComboBox(this);
this->ui_.vbox = new QVBoxLayout(this);
this->ui_.buttonBox = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
QObject::connect(&this->ui_.okButton, &QPushButton::clicked, this,
QObject::connect(this->ui_.buttonBox, &QDialogButtonBox::accepted, this,
&QualityPopup::okButtonClicked);
QObject::connect(&this->ui_.cancelButton, &QPushButton::clicked, this,
QObject::connect(this->ui_.buttonBox, &QDialogButtonBox::rejected, this,
&QualityPopup::cancelButtonClicked);
this->ui_.buttonBox.addButton(&this->ui_.okButton,
QDialogButtonBox::ButtonRole::AcceptRole);
this->ui_.buttonBox.addButton(&this->ui_.cancelButton,
QDialogButtonBox::ButtonRole::RejectRole);
this->ui_.selector->addItems(options);
this->ui_.selector.addItems(options);
this->ui_.vbox->addWidget(this->ui_.selector);
this->ui_.vbox->addWidget(this->ui_.buttonBox);
this->ui_.vbox.addWidget(&this->ui_.selector);
this->ui_.vbox.addWidget(&this->ui_.buttonBox);
this->setLayout(&this->ui_.vbox);
this->setLayout(this->ui_.vbox);
}
void QualityPopup::showDialog(const QString &channelName, QStringList options)
void QualityPopup::showDialog(const QString &channelURL, QStringList options)
{
QualityPopup *instance = new QualityPopup(channelName, options);
QualityPopup *instance = new QualityPopup(channelURL, options);
instance->window()->setWindowTitle("Chatterino - select stream quality");
instance->setAttribute(Qt::WA_DeleteOnClose, true);
@ -43,16 +40,27 @@ void QualityPopup::showDialog(const QString &channelName, QStringList options)
instance->show();
instance->activateWindow();
instance->raise();
instance->setFocus();
}
void QualityPopup::keyPressEvent(QKeyEvent *e)
{
if (this->handleEscape(e, this->ui_.buttonBox))
{
return;
}
if (this->handleEnter(e, this->ui_.buttonBox))
{
return;
}
BasePopup::keyPressEvent(e);
}
void QualityPopup::okButtonClicked()
{
QString channelURL = "twitch.tv/" + this->channelName_;
try
{
openStreamlink(channelURL, this->ui_.selector.currentText());
openStreamlink(this->channelURL_, this->ui_.selector->currentText());
}
catch (const Exception &ex)
{

View file

@ -4,7 +4,6 @@
#include <QComboBox>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QVBoxLayout>
namespace chatterino {
@ -12,22 +11,23 @@ namespace chatterino {
class QualityPopup : public BasePopup
{
public:
QualityPopup(const QString &_channelName, QStringList options);
static void showDialog(const QString &_channelName, QStringList options);
QualityPopup(const QString &channelURL, QStringList options);
static void showDialog(const QString &channelURL, QStringList options);
protected:
void keyPressEvent(QKeyEvent *e) override;
private:
void okButtonClicked();
void cancelButtonClicked();
struct {
QVBoxLayout vbox;
QComboBox selector;
QDialogButtonBox buttonBox;
QPushButton okButton;
QPushButton cancelButton;
QVBoxLayout *vbox;
QComboBox *selector;
QDialogButtonBox *buttonBox;
} ui_;
QString channelName_;
QString channelURL_;
};
} // namespace chatterino

View file

@ -33,7 +33,7 @@
const QString TEXT_VIEWS("Views: %1");
const QString TEXT_FOLLOWERS("Followers: %1");
const QString TEXT_CREATED("Created: %1");
const QString TEXT_TITLE("%1's Usercard");
const QString TEXT_TITLE("%1's Usercard - #%2");
#define TEXT_USER_ID "ID: "
#define TEXT_UNAVAILABLE "(not available)"
@ -513,7 +513,7 @@ void UserInfoPopup::setData(const QString &name, const ChannelPtr &channel)
{
this->userName_ = name;
this->channel_ = channel;
this->setWindowTitle(TEXT_TITLE.arg(name));
this->setWindowTitle(TEXT_TITLE.arg(name, channel->getName()));
this->ui_.nameLabel->setText(name);
this->ui_.nameLabel->setProperty("copy-text", name);
@ -598,7 +598,8 @@ void UserInfoPopup::updateUserData()
this->avatarUrl_ = user.profileImageUrl;
this->ui_.nameLabel->setText(user.displayName);
this->setWindowTitle(TEXT_TITLE.arg(user.displayName));
this->setWindowTitle(
TEXT_TITLE.arg(user.displayName, this->channel_->getName()));
this->ui_.viewCountLabel->setText(
TEXT_VIEWS.arg(localizeNumbers(user.viewCount)));
this->ui_.createdDateLabel->setText(

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

View file

@ -64,6 +64,16 @@ NotebookTab::NotebookTab(Notebook *notebook)
this->notebook_->removePage(this->page);
});
this->menu_.addAction(
"Popup Tab",
[=]() {
if (auto container = dynamic_cast<SplitContainer *>(this->page))
{
container->popup();
}
},
QKeySequence("Ctrl+Shift+N"));
highlightNewMessagesAction_ =
new QAction("Mark Tab as Unread on New Messages", &this->menu_);
highlightNewMessagesAction_->setCheckable(true);

View file

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

@ -520,11 +520,11 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Title", s.headerStreamTitle);
layout.addSubtitle("R9K");
layout.addDescription(
"Hide similar messages by the same user. Toggle hidden "
"messages by pressing Ctrl+H.");
layout.addDescription("Hide similar messages. Toggle hidden "
"messages by pressing Ctrl+H.");
layout.addCheckbox("Hide similar messages", s.similarityEnabled);
//layout.addCheckbox("Gray out matches", s.colorSimilarDisabled);
layout.addCheckbox("By the same user", s.hideSimilarBySameUser);
layout.addCheckbox("Hide my own messages", s.hideSimilarMyself);
layout.addCheckbox("Receive notification sounds from hidden messages",
s.shownSimilarTriggerHighlights);

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

@ -49,7 +49,7 @@ void addPhrasesTab(LayoutCreator<QVBoxLayout> layout)
->initialized(&getSettings()->ignoredMessages))
.getElement();
view->setTitles(
{"Pattern", "Regex", "Case Sensitive", "Block", "Replacement"});
{"Pattern", "Regex", "Case-sensitive", "Block", "Replacement"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(

View file

@ -44,6 +44,8 @@ KeyboardSettingsPage::KeyboardSettingsPage()
form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab"));
form->addRow(new QLabel("Ctrl + Shift + W"),
new QLabel("Close current tab"));
form->addRow(new QLabel("Ctrl + Shift + N"),
new QLabel("Open current tab as a popup"));
form->addRow(new QLabel("Ctrl + H"),
new QLabel("Hide/Show similar messages (See General->R9K)"));

View file

@ -29,19 +29,22 @@ NicknamesPage::NicknamesPage()
->initialized(&getSettings()->nicknames))
.getElement();
view->setTitles({"Username", "Nickname"});
view->setTitles({"Username", "Nickname", "Enable regex", "Case-sensitive"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Interactive);
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
1, QHeaderView::Stretch);
view->addButtonPressed.connect([] {
getSettings()->nicknames.append(Nickname{"Username", "Nickname"});
getSettings()->nicknames.append(
Nickname{"Username", "Nickname", false, false});
});
QTimer::singleShot(1, [view] {
view->getTableView()->resizeColumnsToContents();
view->getTableView()->setColumnWidth(0, 250);
view->getTableView()->setColumnWidth(0, 200);
});
}

View file

@ -262,10 +262,11 @@ Split::Split(QWidget *parent)
if (getSettings()->askOnImageUpload.getValue())
{
QMessageBox msgBox;
msgBox.setWindowTitle("Chatterino");
msgBox.setText("Image upload");
msgBox.setInformativeText(
"You are uploading an image to a 3rd party service not in "
"control of the chatterino team. You may not be able to "
"control of the Chatterino team. You may not be able to "
"remove the image from the site. Are you okay with this?");
msgBox.addButton(QMessageBox::Cancel);
msgBox.addButton(QMessageBox::Yes);
@ -624,8 +625,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 +871,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

@ -9,6 +9,7 @@
#include "util/Helpers.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include "widgets/splits/ClosedSplits.hpp"
@ -761,6 +762,33 @@ void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode)
this->layout();
}
void SplitContainer::popup()
{
Window &window = getApp()->windows->createWindow(WindowType::Popup);
auto popupContainer = window.getNotebook().getOrAddSelectedPage();
QJsonObject encodedTab;
WindowManager::encodeTab(this, true, encodedTab);
TabDescriptor tab = TabDescriptor::loadFromJSON(encodedTab);
// custom title
if (!tab.customTitle_.isEmpty())
{
popupContainer->getTab()->setCustomTitle(tab.customTitle_);
}
// highlighting on new messages
popupContainer->getTab()->setHighlightsEnabled(tab.highlightsEnabled_);
// splits
if (tab.rootNode_)
{
popupContainer->applyFromDescriptor(*tab.rootNode_);
}
window.show();
}
void SplitContainer::applyFromDescriptorRecursively(
const NodeDescriptor &rootNode, Node *node)
{

View file

@ -202,6 +202,8 @@ public:
void applyFromDescriptor(const NodeDescriptor &rootNode);
void popup();
protected:
void paintEvent(QPaintEvent *event) override;

View file

@ -916,10 +916,6 @@ void SplitHeader::themeChangedEvent()
}
}
void SplitHeader::moveSplit()
{
}
void SplitHeader::reloadChannelEmotes()
{
auto channel = this->split_->getChannel();
@ -933,7 +929,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);
this->ui_.hbox->setMargin(
@ -635,9 +639,6 @@ void SplitInput::editTextChanged()
this->textChanged.invoke(text);
text = text.trimmed();
static QRegularExpression spaceRegex("\\s\\s+");
text = text.replace(spaceRegex, " ");
text =
app->commands->execCommand(text, this->split_->getChannel(), true);
}

View file

@ -2,6 +2,7 @@ project(chatterino-test)
set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp
@ -12,6 +13,7 @@ set(test_SOURCES
${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,113 @@
#include "common/ChannelChatters.hpp"
#include <gtest/gtest.h>
#include <QColor>
#include <QStringList>
namespace chatterino {
class MockChannel : public Channel
{
public:
MockChannel(const QString &name)
: Channel(name, Channel::Type::Twitch)
{
}
};
} // namespace chatterino
using namespace chatterino;
// Ensure inserting the same user does not increase the size of the stored colors
TEST(ChatterChatters, insertSameUser)
{
MockChannel channel("test");
ChannelChatters chatters(channel);
EXPECT_EQ(chatters.colorsSize(), 0);
chatters.setUserColor("pajlada", QColor("#fff"));
EXPECT_EQ(chatters.colorsSize(), 1);
chatters.setUserColor("pajlada", QColor("#fff"));
EXPECT_EQ(chatters.colorsSize(), 1);
}
// Ensure we can update a chatters color
TEST(ChatterChatters, insertSameUserUpdatesColor)
{
MockChannel channel("test");
ChannelChatters chatters(channel);
chatters.setUserColor("pajlada", QColor("#fff"));
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#fff"));
chatters.setUserColor("pajlada", QColor("#f0f"));
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#f0f"));
}
// Ensure getting a non-existant users color returns an invalid QColor
TEST(ChatterChatters, getNonExistantUser)
{
MockChannel channel("test");
ChannelChatters chatters(channel);
EXPECT_EQ(chatters.getUserColor("nonexistantuser"), QColor());
}
// Ensure getting a user doesn't create an entry
TEST(ChatterChatters, getDoesNotCreate)
{
MockChannel channel("test");
ChannelChatters chatters(channel);
EXPECT_EQ(chatters.colorsSize(), 0);
chatters.getUserColor("nonexistantuser");
EXPECT_EQ(chatters.colorsSize(), 0);
}
// Ensure the least recently used entry is purged when we reach MAX_SIZE
TEST(ChatterChatters, insertMaxSize)
{
MockChannel channel("test");
ChannelChatters chatters(channel);
// Prime chatters with 2 control entries
chatters.setUserColor("pajlada", QColor("#f00"));
chatters.setUserColor("zneix", QColor("#f0f"));
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#f00"));
EXPECT_EQ(chatters.getUserColor("zneix"), QColor("#f0f"));
EXPECT_EQ(chatters.getUserColor("nonexistantuser"), QColor());
EXPECT_EQ(chatters.colorsSize(), 2);
for (int i = 0; i < ChannelChatters::maxChatterColorCount - 1; ++i)
{
auto username = QString("user%1").arg(i);
chatters.setUserColor(username, QColor("#00f"));
}
// Should have bumped ONE entry out (pajlada)
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor());
EXPECT_EQ(chatters.getUserColor("zneix"), QColor("#f0f"));
EXPECT_EQ(chatters.getUserColor("user1"), QColor("#00f"));
chatters.setUserColor("newuser", QColor("#00e"));
for (int i = 0; i < ChannelChatters::maxChatterColorCount; ++i)
{
auto username = QString("user%1").arg(i);
chatters.setUserColor(username, QColor("#00f"));
}
// One more entry should be bumped out (zneix)
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor());
EXPECT_EQ(chatters.getUserColor("zneix"), QColor());
EXPECT_EQ(chatters.getUserColor("user1"), QColor("#00f"));
}