diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b10d39116..b23fa1f2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,29 +63,32 @@ jobs: version: ${{ matrix.qt-version }} # WINDOWS - - name: Cache conan packages if: startsWith(matrix.os, 'windows') uses: actions/cache@v2.1.4 with: - key: ${{ runner.os }}-conan-${{ hashFiles('**/conanfile.txt') }}-20210307 + key: ${{ runner.os }}-conan-${{ hashFiles('**/conanfile.txt') }}-20210412 path: C:/.conan/ + - name: Add Conan to path + if: startsWith(matrix.os, 'windows') + run: echo "C:\Program Files\Conan\conan\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install dependencies (Windows) if: startsWith(matrix.os, 'windows') run: | choco install conan -y - - refreshenv - shell: cmd + + - name: Enable Developer Command Prompt + if: startsWith(matrix.os, 'windows') + uses: ilammy/msvc-dev-cmd@v1.7.0 - name: Build (Windows) if: startsWith(matrix.os, 'windows') && matrix.build-system == 'qmake' run: | - call "%programfiles(x86)%\Microsoft Visual Studio\%vs_version%\Enterprise\VC\Auxiliary\Build\vcvars64.bat" mkdir build cd build - "C:\Program Files\Conan\conan\conan.exe" install .. + conan install .. qmake .. set cl=/MP nmake /S /NOLOGO @@ -93,15 +96,13 @@ jobs: cp release/chatterino.exe Chatterino2/ echo nightly > Chatterino2/modes 7z a chatterino-windows-x86-64.zip Chatterino2/ - shell: cmd - + - name: Build with CMake (Windows) if: startsWith(matrix.os, 'windows') && matrix.build-system == 'cmake' run: | - call "%programfiles(x86)%\Microsoft Visual Studio\%vs_version%\Enterprise\VC\Auxiliary\Build\vcvars64.bat" mkdir build cd build - "C:\Program Files\Conan\conan\conan.exe" install .. + conan install .. cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DUSE_CONAN=ON .. set cl=/MP nmake /S /NOLOGO @@ -109,17 +110,10 @@ jobs: cp bin/chatterino.exe Chatterino2/ echo nightly > Chatterino2/modes 7z a chatterino-windows-x86-64.zip Chatterino2/ - shell: cmd - - - name: Ensure build succeeded (Windows) - if: startsWith(matrix.os, 'windows') - run: | - cd build - ls Chatterino2/chatterino.exe - name: Upload artifact (Windows) if: startsWith(matrix.os, 'windows') - uses: actions/upload-artifact@v2.2.2 + uses: actions/upload-artifact@v2.2.3 with: name: chatterino-windows-x86-64-${{ matrix.qt-version }}-${{ matrix.build-system }}.zip path: build/chatterino-windows-x86-64.zip @@ -176,7 +170,7 @@ jobs: - name: Upload artifact (Ubuntu) if: startsWith(matrix.os, 'ubuntu') - uses: actions/upload-artifact@v2.2.2 + uses: actions/upload-artifact@v2.2.3 with: name: Chatterino-x86_64-${{ matrix.qt-version }}-${{ matrix.build-system }}.AppImage path: build/Chatterino-x86_64.AppImage @@ -221,7 +215,7 @@ jobs: - name: Upload artifact (MacOS) if: startsWith(matrix.os, 'macos') - uses: actions/upload-artifact@v2.2.2 + uses: actions/upload-artifact@v2.2.3 with: name: chatterino-osx-${{ matrix.qt-version }}-${{ matrix.build-system }}.dmg path: build/chatterino-osx.dmg @@ -245,32 +239,32 @@ jobs: Nightly Build prerelease: true - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: chatterino-windows-x86-64-5.15.2-qmake.zip path: windows/ - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: chatterino-windows-x86-64-5.15.2-cmake.zip path: windows-cmake/ - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: Chatterino-x86_64-5.15.2-qmake.AppImage path: linux/ - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: Chatterino-x86_64-5.15.2-cmake.AppImage path: linux-cmake/ - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: chatterino-osx-5.15.2-qmake.dmg path: macos/ - - uses: actions/download-artifact@v2.0.8 + - uses: actions/download-artifact@v2.0.9 with: name: chatterino-osx-5.15.2-cmake.dmg path: macos-cmake/ diff --git a/.gitmodules b/.gitmodules index 1467b973c..65a01c9a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "lib/websocketpp"] path = lib/websocketpp url = https://github.com/ziocleto/websocketpp +[submodule "cmake/sanitizers-cmake"] + path = cmake/sanitizers-cmake + url = https://github.com/arsenm/sanitizers-cmake diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 878d15a11..401741a27 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -6,8 +6,25 @@ Note on Qt version compatibility: If you are installing Qt from a package manage _most likely works the same for other Debian-like distros_ -1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake` -1. Open `chatterino.pro` with QT Creator and build +1. Install dependencies `sudo apt install qttools5-dev qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++` + +### Through Qt Creator + +1. Install C++ IDE Qt Creator `sudo apt install qtcreator` +1. Open `chatterino.pro` with Qt Creator and select build + +### Manually + +1. go into project directory +1. create build folder `mkdir build && cd build` + +#### Using QMake + +1. `qmake .. && make` + +#### Using CMake + +1. `cmake .. && make` ## Arch Linux @@ -17,15 +34,15 @@ _most likely works the same for other Debian-like distros_ ### Manually -1. `sudo pacman -S qt5-base qt5-multimedia qt5-svg gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake` +1. `sudo pacman -S qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake` 1. go into project directory 1. create build folder `mkdir build && cd build` -### Using QMake +#### Using QMake 1. `qmake .. && make` -### Using CMake +#### Using CMake 1. `cmake .. && make` diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index 16c890211..e8abd49e5 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -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.1i`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1i.exe)** +1. Download OpenSSL for windows, version `1.1.1j`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1j.exe)** 2. When prompted, install OpenSSL to `C:\local\openssl` 3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory". diff --git a/CHANGELOG.md b/CHANGELOG.md index df020dd91..fbe9bad51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ - Major: Added clip creation support. You can create clips with `/clip` command, `Alt+X` keybind or `Create a clip` option in split header's context menu. This requires a new authentication scope so re-authentication will be required to use it. (#2271, #2377, #2528) - Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748, #2083, #2090, #2200, #2225) - Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001, #2316, #2342, #2376) -- Major: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284) +- Major: Add `/settitle` and `/setgame` commands, originally made for Mm2PL/Dankerino. (#2534, #2609) +- Major: Color mentions to match the mentioned users. You can disable this by unchecking "Color @usernames" under `Settings -> General -> Advanced (misc.)`. (#1963, #2284, #2597) - Major: Commands `/ignore` and `/unignore` have been renamed to `/block` and `/unblock` in order to keep consistency with Twitch's terms. (#2370) - Major: Added support for bit emotes - the ones you unlock after cheering to streamer. (#2550) - Minor: Added `/clearmessages` command - does what "Burger menu -> More -> Clear messages" does. (#2485) @@ -43,7 +44,7 @@ - Minor: Show channels live now enabled by default - Minor: Bold usernames enabled by default - Minor: Improve UX of the "Login expired!" message (#2029) -- Minor: PageUp and PageDown now scroll in the selected split (#2070, #2081) +- Minor: PageUp and PageDown now scroll in the selected split and in the emote popup (#2070, #2081, #2410, #2607) - Minor: Allow highlights to be excluded from `/mentions`. Excluded highlights will not trigger tab highlights either. (#1793, #2036) - Minor: Flag all popup dialogs as actual dialogs so they get the relevant window manager hints (#1843, #2182, #2185, #2232, #2234) - Minor: Don't show update button for nightly builds on macOS and Linux, this was already the case for Windows (#2163, #2164) @@ -59,6 +60,7 @@ - Minor: Added `/streamlink` command. Usage: `/streamlink `. You can also use the command without arguments in any twitch channel to open it in streamlink. (#2443, #2495) - Minor: Humanized all numbers visible to end-users. (#2488) - Minor: Added a context menu to avatar in usercard. It opens on right-clicking the avatar in usercard. (#2517) +- Minor: Handle messages that users can share after unlocking a new bits badge. (#2611) - Bugfix: Fix crash occurring when pressing Escape in the Color Picker Dialog (#1843) - Bugfix: Fix bug where the "check user follow state" event could trigger a network request requesting the user to follow or unfollow a user. By itself its quite harmless as it just repeats to Twitch the same follow state we had, so no follows should have been lost by this but it meant there was a rogue network request that was fired that could cause a crash (#1906) - Bugfix: /usercard command will now respect the "Automatically close user popup" setting (#1918) @@ -81,11 +83,14 @@ - Bugfix: Fix anonymous users being pinged by "username" justinfan64537 (#2156, #2352) - Bugfix: Fixed hidden tooltips when always on top is active (#2384) - Bugfix: Fix CLI arguments (`--help`, `--version`, `--channels`) not being respected (#2368, #2190) +- Bugfix: Fixed search field not being focused on popup open (#2540) - Bugfix: Fix Twitch cheer emotes not displaying tooltips when hovered (#2434, #2503) - Bugfix: Fix BTTV/FFZ channel emotes saying unknown error when no emotes found (#2542) - Bugfix: Fix directory not opening when clicking "Open AppData Directory" setting button on macOS (#2531, #2537) - Bugfix: Fix quickswitcher not respecting order of tabs when filtering (#2519, #2561) - Bugfix: Fix GNOME not associating Chatterino's window with its desktop entry (#1863, #2587) +- Bugfix: Fix buffer overflow in emoji parsing. (#2602) +- Bugfix: Fix windows being brought back to life after the settings dialog was closed. (#1892, #2613) - Dev: Updated minimum required Qt framework version to 5.12. (#2210) - Dev: Migrated `Kraken::getUser` to Helix (#2260) - Dev: Migrated `TwitchAccount::(un)followUser` from Kraken to Helix and moved it to `Helix::(un)followUser`. (#2306) diff --git a/CMakeLists.txt b/CMakeLists.txt index c0e79ed88..b4e2d66d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ include(FeatureSummary) list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" + "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" ) project(chatterino VERSION 2.2.3) @@ -44,6 +45,8 @@ if (WIN32) find_package(WinToast REQUIRED) endif () +find_package(Sanitizers) + # Find boost on the system find_package(Boost REQUIRED) diff --git a/cmake/sanitizers-cmake b/cmake/sanitizers-cmake new file mode 160000 index 000000000..99e159ec9 --- /dev/null +++ b/cmake/sanitizers-cmake @@ -0,0 +1 @@ +Subproject commit 99e159ec9bc8dd362b08d18436bd40ff0648417b diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 835c2dcba..b44eae216 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -13,8 +13,7 @@

- Chatterino 2 is the second installment of the Twitch chat client series - "Chatterino". + Chatterino is a chat client for Twitch chat. It aims to be an improved/extended version of the Twitch web chat.

diff --git a/src/BaseSettings.cpp b/src/BaseSettings.cpp index 8d29fccb6..4343e6306 100644 --- a/src/BaseSettings.cpp +++ b/src/BaseSettings.cpp @@ -13,7 +13,7 @@ AB_SETTINGS_CLASS *AB_SETTINGS_CLASS::instance = nullptr; void _actuallyRegisterSetting( std::weak_ptr setting) { - _settings.push_back(setting); + _settings.push_back(std::move(setting)); } AB_SETTINGS_CLASS::AB_SETTINGS_CLASS(const QString &settingsDirectory) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e0ad2e51..397ccb8d0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -460,6 +460,7 @@ endif () source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${SOURCE_FILES}) add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +add_sanitizers(${PROJECT_NAME}) target_precompile_headers(${PROJECT_NAME} PRIVATE PrecompiledHeader.hpp) diff --git a/src/common/Args.cpp b/src/common/Args.cpp index 364c1a919..e6960e824 100644 --- a/src/common/Args.cpp +++ b/src/common/Args.cpp @@ -106,7 +106,7 @@ void Args::applyCustomChannelLayout(const QString &argValue) return QRect(-1, -1, -1, -1); }(); - window.geometry_ = std::move(configMainLayout); + window.geometry_ = configMainLayout; QStringList channelArgList = argValue.split(";"); for (const QString &channelArg : channelArgList) diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index c18271655..9d8b3ba6f 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -320,13 +320,13 @@ void Channel::onConnected() // Indirect channel // IndirectChannel::Data::Data(ChannelPtr _channel, Channel::Type _type) - : channel(_channel) + : channel(std::move(_channel)) , type(_type) { } IndirectChannel::IndirectChannel(ChannelPtr channel, Channel::Type type) - : data_(std::make_unique(channel, type)) + : data_(std::make_unique(std::move(channel), type)) { } @@ -339,7 +339,7 @@ void IndirectChannel::reset(ChannelPtr channel) { assert(this->data_->type != Channel::Type::Direct); - this->data_->channel = channel; + this->data_->channel = std::move(channel); this->data_->changed.invoke(); } diff --git a/src/common/ChatterinoSetting.cpp b/src/common/ChatterinoSetting.cpp index 100babaa6..b0acb854a 100644 --- a/src/common/ChatterinoSetting.cpp +++ b/src/common/ChatterinoSetting.cpp @@ -6,7 +6,7 @@ namespace chatterino { void _registerSetting(std::weak_ptr setting) { - _actuallyRegisterSetting(setting); + _actuallyRegisterSetting(std::move(setting)); } } // namespace chatterino diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 5e4f5678a..1ecfe30ce 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -19,6 +19,7 @@ enum class NetworkRequestType { Post, Put, Delete, + Patch, }; } // namespace chatterino diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index e61bc4fe9..b141cd853 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -116,6 +116,19 @@ void loadUncached(const std::shared_ptr &data) return NetworkManager::accessManager.post( data->request_, data->payload_); } + case NetworkRequestType::Patch: + if (data->multiPartPayload_) + { + assert(data->payload_.isNull()); + + return NetworkManager::accessManager.sendCustomRequest( + data->request_, "PATCH", data->multiPartPayload_); + } + else + { + return NetworkManager::accessManager.sendCustomRequest( + data->request_, "PATCH", data->payload_); + } } return nullptr; }(); diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 2d07dff1d..e6b7a9414 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -643,7 +643,85 @@ void CommandController::initialize(Settings &, Paths &paths) getApp()->windows->getMainWindow().getNotebook().getSelectedPage()); currentPage->getSelectedSplit()->getChannelView().clearMessages(); + return ""; + }); + this->registerCommand("/settitle", [](const QStringList &words, + ChannelPtr channel) { + if (words.size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage: /settitle .")); + return ""; + } + if (auto twitchChannel = dynamic_cast(channel.get())) + { + auto status = twitchChannel->accessStreamStatus(); + auto title = words.mid(1).join(" "); + getHelix()->updateChannel( + twitchChannel->roomId(), "", "", title, + [channel, title](NetworkResult) { + channel->addMessage(makeSystemMessage( + QString("Updated title to %1").arg(title))); + }, + [channel] { + channel->addMessage( + makeSystemMessage("Title update failed! Are you " + "missing the required scope?")); + }); + } + else + { + channel->addMessage(makeSystemMessage( + "Unable to set title of non-Twitch channel.")); + } + return ""; + }); + this->registerCommand("/setgame", [](const QStringList &words, + ChannelPtr channel) { + if (words.size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage: /setgame .")); + return ""; + } + if (auto twitchChannel = dynamic_cast(channel.get())) + { + getHelix()->searchGames( + words.mid(1).join(" "), + [channel, twitchChannel](std::vector games) { + if (games.empty()) + { + channel->addMessage( + makeSystemMessage("Game not found.")); + } + else // 1 or more games + { + auto status = twitchChannel->accessStreamStatus(); + getHelix()->updateChannel( + twitchChannel->roomId(), games.at(0).id, "", "", + [channel, games](NetworkResult) { + channel->addMessage(makeSystemMessage( + QString("Updated game to %1") + .arg(games.at(0).name))); + }, + [channel] { + channel->addMessage(makeSystemMessage( + "Game update failed! Are you " + "missing the required scope?")); + }); + } + }, + [channel] { + channel->addMessage( + makeSystemMessage("Failed to look up game.")); + }); + } + else + { + channel->addMessage( + makeSystemMessage("Unable to set game of non-Twitch channel.")); + } return ""; }); } diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index 0284fb89f..635904131 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -57,7 +57,7 @@ HighlightPhrase::HighlightPhrase(const QString &pattern, bool showInMentions, , isRegex_(isRegex) , isCaseSensitive_(isCaseSensitive) , soundUrl_(soundUrl) - , color_(color) + , color_(std::move(color)) , regex_(isRegex_ ? pattern : REGEX_START_BOUNDARY + QRegularExpression::escape(pattern) + diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index eb2b8b988..b59b12ce7 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -39,7 +39,7 @@ namespace { } // namespace MessageLayout::MessageLayout(MessagePtr message) - : message_(message) + : message_(std::move(message)) , container_(std::make_shared()) { DebugCount::increase("message layout"); diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 3eab41f41..f6b91c3b6 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -88,7 +88,7 @@ FlagsEnum MessageLayoutElement::getFlags() const ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, const QSize &size) : MessageLayoutElement(creator, size) - , image_(image) + , image_(std::move(image)) { this->trailingSpace = creator.hasTrailingSpace(); } diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp index ad35db056..b29488ef3 100644 --- a/src/providers/colors/ColorProvider.cpp +++ b/src/providers/colors/ColorProvider.cpp @@ -26,7 +26,7 @@ const std::shared_ptr ColorProvider::color(ColorType type) const void ColorProvider::updateColor(ColorType type, QColor color) { auto colorPtr = this->typeColorMap_.at(type); - *colorPtr = color; + *colorPtr = std::move(color); } QSet ColorProvider::recentColors() const diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 58cff2591..af8753798 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -24,7 +24,7 @@ namespace { const rapidjson::Value &unparsedEmoji, QString shortCode = QString()) { - static uint unicodeBytes[4]; + std::array unicodeBytes; struct { bool apple; @@ -91,11 +91,12 @@ namespace { for (const QString &unicodeCharacter : unicodeCharacters) { - unicodeBytes[numUnicodeBytes++] = + unicodeBytes.at(numUnicodeBytes++) = QString(unicodeCharacter).toUInt(nullptr, 16); } - emojiData->value = QString::fromUcs4(unicodeBytes, numUnicodeBytes); + emojiData->value = + QString::fromUcs4(unicodeBytes.data(), numUnicodeBytes); } // getToneNames takes a tones and returns their names in the same order diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 71d84fb79..3e794202e 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -14,6 +14,7 @@ #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/FormatTime.hpp" +#include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" #include @@ -579,10 +580,11 @@ std::vector IrcMessageHandler::parseUserNoticeMessage( content = parameters[1]; } - if (msgType == "sub" || msgType == "resub" || msgType == "subgift") + if (msgType == "sub" || msgType == "resub" || msgType == "subgift" || + msgType == "bitsbadgetier") { - // Sub-specific message. I think it's only allowed for "resub" messages - // atm + // Sub-specific and bits badge upgrade specific message. + // It's only allowed for "resub" messages. if (!content.isEmpty()) { MessageParseArgs args; @@ -600,9 +602,18 @@ std::vector IrcMessageHandler::parseUserNoticeMessage( if (it != tags.end()) { - auto b = - MessageBuilder(systemMessage, parseTagString(it.value().toString()), - calculateMessageTimestamp(message)); + QString messageText = it.value().toString(); + + if (msgType == "bitsbadgetier") + { + messageText = QString("%1 just earned a new %2 Bits badge!") + .arg(tags.value("display-name").toString()) + .arg(kFormatNumbers( + tags.value("msg-param-threshold").toInt())); + } + + auto b = MessageBuilder(systemMessage, parseTagString(messageText), + calculateMessageTimestamp(message)); b->flags.set(MessageFlag::Subscription); auto newMessage = b.release(); @@ -628,10 +639,11 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, content = parameters[1]; } - if (msgType == "sub" || msgType == "resub" || msgType == "subgift") + if (msgType == "sub" || msgType == "resub" || msgType == "subgift" || + msgType == "bitsbadgetier") { - // Sub-specific message. I think it's only allowed for "resub" messages - // atm + // Sub-specific and bits badge upgrade specific message. + // It's only allowed for "resub" messages. if (!content.isEmpty()) { this->addMessage(message, target, content, server, true, false); @@ -642,9 +654,18 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, if (it != tags.end()) { - auto b = - MessageBuilder(systemMessage, parseTagString(it.value().toString()), - calculateMessageTimestamp(message)); + QString messageText = it.value().toString(); + + if (msgType == "bitsbadgetier") + { + messageText = QString("%1 just earned a new %2 Bits badge!") + .arg(tags.value("display-name").toString()) + .arg(kFormatNumbers( + tags.value("msg-param-threshold").toInt())); + } + + auto b = MessageBuilder(systemMessage, parseTagString(messageText), + calculateMessageTimestamp(message)); b->flags.set(MessageFlag::Subscription); auto newMessage = b.release(); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 316f1fc77..b0e2a6ccd 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -64,7 +64,7 @@ QColor TwitchAccount::color() void TwitchAccount::setColor(QColor color) { - this->color_.set(color); + this->color_.set(std::move(color)); } bool TwitchAccount::setOAuthClient(const QString &newClientID) @@ -131,7 +131,7 @@ void TwitchAccount::blockUser(QString userId, std::function onSuccess, } onSuccess(); }, - onFailure); + std::move(onFailure)); } void TwitchAccount::unblockUser(QString userId, std::function onSuccess, @@ -149,7 +149,7 @@ void TwitchAccount::unblockUser(QString userId, std::function onSuccess, } onSuccess(); }, - onFailure); + std::move(onFailure)); } void TwitchAccount::checkFollow(const QString targetUserID, diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index aea1f0f45..b47e95034 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -484,6 +484,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) if (match.hasMatch()) { QString username = match.captured(1); + auto originalTextColor = textColor; if (this->twitchChannel != nullptr && getSettings()->colorUsernames) { @@ -495,13 +496,23 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } } - this->emplace(string, MessageElementFlag::BoldUsername, + auto prefixedUsername = '@' + username; + this->emplace(prefixedUsername, + MessageElementFlag::BoldUsername, textColor, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}); + ->setLink({Link::UserInfo, username}) + ->setTrailingSpace(false); + + this->emplace(prefixedUsername, + MessageElementFlag::NonBoldUsername, + textColor) + ->setLink({Link::UserInfo, username}) + ->setTrailingSpace(false); + + this->emplace(string.remove(prefixedUsername), + MessageElementFlag::Text, + originalTextColor); - this->emplace( - string, MessageElementFlag::NonBoldUsername, textColor) - ->setLink({Link::UserInfo, username}); return; } } @@ -514,6 +525,8 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) if (match.hasMatch() && chatters->contains(username)) { + auto originalTextColor = textColor; + if (getSettings()->colorUsernames) { if (auto userColor = @@ -524,13 +537,21 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } } - this->emplace(string, MessageElementFlag::BoldUsername, + this->emplace(username, + MessageElementFlag::BoldUsername, textColor, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}); + ->setLink({Link::UserInfo, username}) + ->setTrailingSpace(false); this->emplace( - string, MessageElementFlag::NonBoldUsername, textColor) - ->setLink({Link::UserInfo, username}); + username, MessageElementFlag::NonBoldUsername, textColor) + ->setLink({Link::UserInfo, username}) + ->setTrailingSpace(false); + + this->emplace(string.remove(username), + MessageElementFlag::Text, + originalTextColor); + return; } } diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 3b6564eae..0af299970 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -58,7 +58,7 @@ void Helix::getUserByName(QString userName, HelixFailureCallback failureCallback) { QStringList userIds; - QStringList userLogins{userName}; + QStringList userLogins{std::move(userName)}; this->fetchUsers( userIds, userLogins, @@ -78,7 +78,7 @@ void Helix::getUserById(QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback) { - QStringList userIds{userId}; + QStringList userIds{std::move(userId)}; QStringList userLogins; this->fetchUsers( @@ -136,7 +136,8 @@ void Helix::getUserFollowers( QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback) { - this->fetchUsersFollows("", userId, successCallback, failureCallback); + this->fetchUsersFollows("", std::move(userId), std::move(successCallback), + std::move(failureCallback)); } void Helix::getUserFollow( @@ -145,7 +146,7 @@ void Helix::getUserFollow( HelixFailureCallback failureCallback) { this->fetchUsersFollows( - userId, targetId, + std::move(userId), std::move(targetId), [successCallback](const auto &response) { if (response.data.empty()) { @@ -155,7 +156,7 @@ void Helix::getUserFollow( successCallback(true, response.data[0]); }, - failureCallback); + std::move(failureCallback)); } void Helix::fetchStreams( @@ -209,7 +210,7 @@ void Helix::getStreamById(QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback) { - QStringList userIds{userId}; + QStringList userIds{std::move(userId)}; QStringList userLogins; this->fetchStreams( @@ -230,7 +231,7 @@ void Helix::getStreamByName(QString userName, HelixFailureCallback failureCallback) { QStringList userIds; - QStringList userLogins{userName}; + QStringList userLogins{std::move(userName)}; this->fetchStreams( userIds, userLogins, @@ -295,11 +296,47 @@ void Helix::fetchGames(QStringList gameIds, QStringList gameNames, .execute(); } +void Helix::searchGames(QString gameName, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + urlQuery.addQueryItem("query", gameName); + + this->makeRequest("search/categories", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector games; + + for (const auto &jsonStream : data.toArray()) + { + games.emplace_back(jsonStream.toObject()); + } + + successCallback(games); + + return Success; + }) + .onError([failureCallback](auto /*result*/) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + void Helix::getGameById(QString gameId, ResultCallback successCallback, HelixFailureCallback failureCallback) { - QStringList gameIds{gameId}; + QStringList gameIds{std::move(gameId)}; QStringList gameNames; this->fetchGames( @@ -409,7 +446,7 @@ void Helix::createClip(QString channelId, break; } }) - .finally(finallyCallback) + .finally(std::move(finallyCallback)) .execute(); } @@ -578,6 +615,48 @@ void Helix::unblockUser(QString targetUserId, .execute(); } +void Helix::updateChannel(QString broadcasterId, QString gameId, + QString language, QString title, + std::function successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + auto data = QJsonDocument(); + auto obj = QJsonObject(); + if (!gameId.isEmpty()) + { + obj.insert("game_id", gameId); + } + if (!language.isEmpty()) + { + obj.insert("broadcaster_language", language); + } + if (!title.isEmpty()) + { + obj.insert("title", title); + } + + if (title.isEmpty() && gameId.isEmpty() && language.isEmpty()) + { + qCDebug(chatterinoCommon) << "Tried to update channel with no changes!"; + return; + } + + data.setObject(obj); + urlQuery.addQueryItem("broadcaster_id", broadcasterId); + this->makeRequest("channels", urlQuery) + .type(NetworkRequestType::Patch) + .header("Content-Type", "application/json") + .payload(data.toJson()) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + successCallback(result); + return Success; + }) + .onError([failureCallback](NetworkResult result) { + failureCallback(); + }) + .execute(); +} NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); @@ -611,8 +690,8 @@ NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) void Helix::update(QString clientId, QString oauthToken) { - this->clientId = clientId; - this->oauthToken = oauthToken; + this->clientId = std::move(clientId); + this->oauthToken = std::move(oauthToken); } void Helix::initialize() diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index eafa04ab9..ea5c7beed 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -252,6 +252,11 @@ public: ResultCallback> successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#search-categories + void searchGames(QString gameName, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + void getGameById(QString gameId, ResultCallback successCallback, HelixFailureCallback failureCallback); @@ -296,6 +301,12 @@ public: std::function successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#modify-channel-information + void updateChannel(QString broadcasterId, QString gameId, QString language, + QString title, + std::function successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/Kraken.cpp b/src/providers/twitch/api/Kraken.cpp index f906b6fb0..5d2a5c398 100644 --- a/src/providers/twitch/api/Kraken.cpp +++ b/src/providers/twitch/api/Kraken.cpp @@ -63,8 +63,8 @@ NetworkRequest Kraken::makeRequest(QString url, QUrlQuery urlQuery) void Kraken::update(QString clientId, QString oauthToken) { - this->clientId = clientId; - this->oauthToken = oauthToken; + this->clientId = std::move(clientId); + this->oauthToken = std::move(oauthToken); } void Kraken::initialize() diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index f86aa986f..779758fe3 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -104,6 +104,16 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information Used in: - `TwitchChannel` to refresh stream title +### Update Channel + +URL: https://dev.twitch.tv/docs/api/reference#modify-channel-information +Requires `channel:manage:broadcast` scope + +- We implement this in `providers/twitch/api/Helix.cpp updateChannel` + Used in: + - `/setgame` to update the game in the current channel + - `/settitle` to update the title in the current channel + ### Create Stream Marker URL: https://dev.twitch.tv/docs/api/reference/#create-stream-marker @@ -142,6 +152,14 @@ Requires `user:manage:blocked_users` scope - `widgets/dialogs/UserInfoPopup.cpp` to unblock a user via checkbox in the usercard - `controllers/commands/CommandController.cpp` to unblock a user via "/unblock" command +### Search Categories + +URL: https://dev.twitch.tv/docs/api/reference#search-categories + +- We implement this in `providers/twitch/api/Helix.cpp searchGames` + Used in: + - `controllers/commands/CommandController.cpp` in `/setgame` command to fuzzy search for game titles + ## TMI The TMI api is undocumented. diff --git a/src/singletons/TooltipPreviewImage.cpp b/src/singletons/TooltipPreviewImage.cpp index 2885d9341..637d2e506 100644 --- a/src/singletons/TooltipPreviewImage.cpp +++ b/src/singletons/TooltipPreviewImage.cpp @@ -33,7 +33,7 @@ TooltipPreviewImage::TooltipPreviewImage() void TooltipPreviewImage::setImage(ImagePtr image) { - this->image_ = image; + this->image_ = std::move(image); this->refreshTooltipWidgetPixmap(); } diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index 2abd47edb..e8bbbd134 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -42,4 +42,9 @@ QString localizeNumbers(const int &number) return locale.toString(number); } +QString kFormatNumbers(const int &number) +{ + return QString("%1K").arg(number / 1000); +} + } // namespace chatterino diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 897f84750..23c4d4f21 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -15,4 +15,6 @@ QString shortenString(const QString &str, unsigned maxWidth = 50); QString localizeNumbers(const int &number); +QString kFormatNumbers(const int &number); + } // namespace chatterino diff --git a/src/util/PostToThread.hpp b/src/util/PostToThread.hpp index ab0fbe1cb..5f90849d7 100644 --- a/src/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -17,7 +17,7 @@ class LambdaRunnable : public QRunnable public: LambdaRunnable(std::function action) { - this->action_ = action; + this->action_ = std::move(action); } void run() diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index f1f4ea6df..15c1f6dc9 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -143,7 +143,7 @@ void AttachedWindow::detach(const QString &winId) void AttachedWindow::setChannel(ChannelPtr channel) { - this->ui_.split->setChannel(channel); + this->ui_.split->setChannel(std::move(channel)); } void AttachedWindow::showEvent(QShowEvent *) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 3466c08b0..649f2f6da 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -244,7 +244,10 @@ void BaseWindow::init() getSettings()->windowTopMost.connect( [this](bool topMost, auto) { this->setWindowFlag(Qt::WindowStaysOnTopHint, topMost); - this->show(); + if (this->isVisible()) + { + this->show(); + } }, this->managedConnections_); } diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index e61a32b60..7f5e8a4ce 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -5,13 +5,13 @@ namespace chatterino { Label::Label(QString text, FontStyle style) - : Label(nullptr, text, style) + : Label(nullptr, std::move(text), style) { } Label::Label(BaseWidget *parent, QString text, FontStyle style) : BaseWidget(parent) - , text_(text) + , text_(std::move(text)) , fontStyle_(style) { this->connections_.managedConnect(getFonts()->fontChanged, [this] { diff --git a/src/widgets/StreamView.cpp b/src/widgets/StreamView.cpp index 65118b386..a47450b44 100644 --- a/src/widgets/StreamView.cpp +++ b/src/widgets/StreamView.cpp @@ -26,7 +26,7 @@ StreamView::StreamView(ChannelPtr channel, const QUrl &url) auto chat = layoutCreator.emplace(); chat->setFixedWidth(300); - chat->setChannel(channel); + chat->setChannel(std::move(channel)); this->layout()->setSpacing(0); this->layout()->setMargin(0); diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index df40b720e..812bbadb1 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -11,6 +11,7 @@ #include "singletons/WindowManager.hpp" #include "util/Shortcut.hpp" #include "widgets/Notebook.hpp" +#include "widgets/Scrollbar.hpp" #include "widgets/helper/ChannelView.hpp" #include @@ -178,6 +179,20 @@ EmotePopup::EmotePopup(QWidget *parent) createWindowShortcut(this, "CTRL+Shift+Tab", [=] { notebook->selectPreviousTab(); }); + + // Scroll with Page Up / Page Down + createWindowShortcut(this, "PgUp", [=] { + auto &scrollbar = + dynamic_cast(notebook->getSelectedPage()) + ->getScrollBar(); + scrollbar.offset(-scrollbar.getLargeChange()); + }); + createWindowShortcut(this, "PgDown", [=] { + auto &scrollbar = + dynamic_cast(notebook->getSelectedPage()) + ->getScrollBar(); + scrollbar.offset(scrollbar.getLargeChange()); + }); } void EmotePopup::loadChannel(ChannelPtr _channel) diff --git a/src/widgets/dialogs/NotificationPopup.cpp b/src/widgets/dialogs/NotificationPopup.cpp index 539da739a..95233db6e 100644 --- a/src/widgets/dialogs/NotificationPopup.cpp +++ b/src/widgets/dialogs/NotificationPopup.cpp @@ -45,7 +45,7 @@ void NotificationPopup::updatePosition() void NotificationPopup::addMessage(MessagePtr msg) { - this->channel_->addMessage(msg); + this->channel_->addMessage(std::move(msg)); // QTimer::singleShot(5000, this, [this, msg] { this->channel->remove }); } diff --git a/src/widgets/helper/Button.cpp b/src/widgets/helper/Button.cpp index 7ad648097..0d7067937 100644 --- a/src/widgets/helper/Button.cpp +++ b/src/widgets/helper/Button.cpp @@ -38,7 +38,7 @@ Button::Button(BaseWidget *parent) void Button::setMouseEffectColor(boost::optional color) { - this->mouseEffectColor_ = color; + this->mouseEffectColor_ = std::move(color); } void Button::setPixmap(const QPixmap &_pixmap) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 0d91ff315..01e313383 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -550,7 +550,7 @@ bool ChannelView::getEnableScrollingToBottom() const void ChannelView::setOverrideFlags(boost::optional value) { - this->overrideFlags_ = value; + this->overrideFlags_ = std::move(value); } const boost::optional &ChannelView::getOverrideFlags() @@ -647,7 +647,7 @@ void ChannelView::setChannel(ChannelPtr underlyingChannel) this->channelConnections_.push_back(this->channel_->messageAppended.connect( [this](MessagePtr &message, boost::optional overridingFlags) { - this->messageAppended(message, overridingFlags); + this->messageAppended(message, std::move(overridingFlags)); })); this->channelConnections_.push_back( @@ -753,7 +753,7 @@ ChannelPtr ChannelView::sourceChannel() const void ChannelView::setSourceChannel(ChannelPtr sourceChannel) { - this->sourceChannel_ = sourceChannel; + this->sourceChannel_ = std::move(sourceChannel); } bool ChannelView::hasSourceChannel() const diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index e213c8fbd..a0b54da8a 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -66,7 +66,7 @@ SearchPopup::SearchPopup(QWidget *parent) void SearchPopup::setChannelFilters(FilterSetPtr filters) { - this->channelFilters_ = filters; + this->channelFilters_ = std::move(filters); } void SearchPopup::setChannel(const ChannelPtr &channel) @@ -155,6 +155,8 @@ void SearchPopup::initLayout() this->setLayout(layout1); } + + this->searchInput_->setFocus(); } std::vector> SearchPopup::parsePredicates( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dc47bea95..941b000fb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(${PROJECT_NAME} ${chatterino_SOURCES} ${test_SOURCES} ) +add_sanitizers(${PROJECT_NAME}) # Enable autogeneration of Qts MOC/RCC/UIC set_target_properties(${PROJECT_NAME}