diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21b32481f48..87a9b974f2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,6 @@ jobs: plugins: false skip-artifact: false skip-crashpad: false - clang-tidy-review: false # Ubuntu 22.04, Qt 5.15 - os: ubuntu-22.04 qt-version: 5.15.2 @@ -45,7 +44,6 @@ jobs: plugins: false skip-artifact: false skip-crashpad: false - clang-tidy-review: true # Ubuntu 22.04, Qt 6.2.4 - tests LTO & plugins - os: ubuntu-22.04 qt-version: 6.2.4 @@ -53,7 +51,6 @@ jobs: plugins: true skip-artifact: false skip-crashpad: false - clang-tidy-review: false # macOS - os: macos-latest qt-version: 5.15.2 @@ -61,7 +58,6 @@ jobs: plugins: false skip-artifact: false skip-crashpad: false - clang-tidy-review: false # Windows - os: windows-latest qt-version: 6.5.0 @@ -69,7 +65,6 @@ jobs: plugins: false skip-artifact: false skip-crashpad: false - clang-tidy-review: false # Windows 7/8 - os: windows-latest qt-version: 5.15.2 @@ -77,7 +72,6 @@ jobs: plugins: false skip-artifact: false skip-crashpad: true - clang-tidy-review: false fail-fast: false @@ -345,38 +339,6 @@ jobs: make -j"$(nproc)" shell: bash - - name: clang-tidy review - if: matrix.clang-tidy-review && github.event_name == 'pull_request' - timeout-minutes: 10 - uses: ZedThree/clang-tidy-review@v0.14.0 - with: - build_dir: build-clang-tidy - config_file: ".clang-tidy" - split_workflow: true - exclude: "lib/*" - cmake_command: >- - cmake -S. -Bbuild-clang-tidy - -DCMAKE_BUILD_TYPE=Release - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On - -DUSE_PRECOMPILED_HEADERS=OFF - -DCMAKE_EXPORT_COMPILE_COMMANDS=On - -DCHATTERINO_LTO=Off - -DCHATTERINO_PLUGINS=On - -DBUILD_WITH_QT6=Off - -DBUILD_TESTS=On - -DBUILD_BENCHMARKS=On - apt_packages: >- - qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, - libsecret-1-dev, - libboost-dev, libboost-system-dev, libboost-filesystem-dev, - libssl-dev, - rapidjson-dev, - libbenchmark-dev - - - name: clang-tidy-review upload - if: matrix.clang-tidy-review && github.event_name == 'pull_request' - uses: ZedThree/clang-tidy-review/upload@v0.14.0 - - name: Package - AppImage (Ubuntu) if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact run: | diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 00000000000..96468c137af --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,148 @@ +--- +name: clang-tidy + +on: + pull_request: + +concurrency: + group: clang-tidy-${{ github.ref }} + cancel-in-progress: true + +env: + CHATTERINO_REQUIRE_CLEAN_GIT: On + C2_BUILD_WITH_QT6: Off + +jobs: + build: + name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # Ubuntu 22.04, Qt 5.15 + - os: ubuntu-22.04 + qt-version: 5.15.2 + plugins: false + + fail-fast: false + + steps: + - name: Enable plugin support + if: matrix.plugins + run: | + echo "C2_PLUGINS=ON" >> "$GITHUB_ENV" + shell: bash + + - name: Set BUILD_WITH_QT6 + if: startsWith(matrix.qt-version, '6.') + run: | + echo "C2_BUILD_WITH_QT6=ON" >> "$GITHUB_ENV" + shell: bash + + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 # allows for tags access + + - name: Install Qt5 + if: startsWith(matrix.qt-version, '5.') + uses: jurplel/install-qt-action@v3.3.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + version: ${{ matrix.qt-version }} + + - name: Install Qt 6.5.3 imageformats + if: startsWith(matrix.qt-version, '6.') + uses: jurplel/install-qt-action@v3.3.0 + with: + cache: false + modules: qtimageformats + set-env: false + version: 6.5.3 + extra: --noarchives + + - name: Install Qt6 + if: startsWith(matrix.qt-version, '6.') + uses: jurplel/install-qt-action@v3.3.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: qt5compat qtimageformats + version: ${{ matrix.qt-version }} + + # LINUX + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 + + - name: Apply Qt5 patches + if: startsWith(matrix.qt-version, '5.') + run: | + patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch + shell: bash + + - name: Build + run: | + mkdir build + cd build + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_BUILD_TYPE=Release \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO="$C2_ENABLE_LTO" \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + .. + shell: bash + + - name: clang-tidy review + timeout-minutes: 20 + uses: ZedThree/clang-tidy-review@v0.14.0 + with: + build_dir: build-clang-tidy + config_file: ".clang-tidy" + split_workflow: true + exclude: "lib/*" + cmake_command: >- + cmake -S. -Bbuild-clang-tidy + -DCMAKE_BUILD_TYPE=Release + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On + -DUSE_PRECOMPILED_HEADERS=OFF + -DCMAKE_EXPORT_COMPILE_COMMANDS=On + -DCHATTERINO_LTO=Off + -DCHATTERINO_PLUGINS=On + -DBUILD_WITH_QT6=Off + -DBUILD_TESTS=On + -DBUILD_BENCHMARKS=On + apt_packages: >- + qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, + libsecret-1-dev, + libboost-dev, libboost-system-dev, libboost-filesystem-dev, + libssl-dev, + rapidjson-dev, + libbenchmark-dev + + - name: clang-tidy-review upload + uses: ZedThree/clang-tidy-review/upload@v0.14.0 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index e611a24f086..92ef0820cdb 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -3,13 +3,15 @@ name: Post clang-tidy review comments on: workflow_run: - workflows: ["Build"] + workflows: ["clang-tidy"] types: - completed jobs: build: runs-on: ubuntu-latest + # Only when a build succeeds + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - uses: ZedThree/clang-tidy-review/post@v0.14.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcff60f4ae..48af80be1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Upstream - Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922) +- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986) - Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) - Minor: The account switcher is now styled to match your theme. (#4817) - Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) @@ -19,6 +20,7 @@ - Minor: The `/reply` command now replies to the latest message of the user. (#4919) - Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) - Minor: Add an option to use new experimental smarter emote completion. (#4987) +- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) @@ -49,6 +51,8 @@ - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965) - Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) - Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971) +- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994) +- Bugfix: Fixed some windows appearing between screens. (#4797) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) @@ -67,6 +71,7 @@ - Dev: Add a compile-time flag `CHATTERINO_UPDATER` which can be turned off to disable update checks. (#4854) - Dev: Add a compile-time flag `USE_SYSTEM_MINIAUDIO` which can be turned on to use the system miniaudio. (#4867) - Dev: Update vcpkg to use Qt6. (#4872) +- Dev: Update `magic_enum` to v0.9.5. (#4992) - Dev: Replace `boost::optional` with `std::optional`. (#4877) - Dev: Improve performance of selecting text. (#4889, #4911) - Dev: Removed direct dependency on Qt 5 compatibility module. (#4906) @@ -79,6 +84,7 @@ - Dev: `Details` file properties tab is now populated on Windows. (#4912) - Dev: Removed `Outcome` from network requests. (#4959) - Dev: Added Tests for Windows and MacOS in CI. (#4970) +- Dev: Move `clang-tidy` checker to its own CI job. (#4996) - Dev: Refactored the Image Uploader feature. (#4971) - Dev: Fixed deadlock and use-after-free in tests. (#4981) diff --git a/benchmarks/.clang-format b/benchmarks/.clang-format index 7bae09f2ce3..0feaad9dc10 100644 --- a/benchmarks/.clang-format +++ b/benchmarks/.clang-format @@ -32,9 +32,6 @@ IncludeCategories: # Project includes - Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$' Priority: 1 - # Third party library includes - - Regex: '<[[:alnum:].]+/[a-zA-Z0-9\._\/-]+>' - Priority: 3 # Qt includes - Regex: '^$' Priority: 3 @@ -42,12 +39,12 @@ IncludeCategories: # LibCommuni includes - Regex: "^$" Priority: 3 - # Misc libraries - - Regex: '^<[a-zA-Z_0-9]+\.h(pp)?>$' - Priority: 3 # Standard library includes - Regex: "^<[a-zA-Z_]+>$" Priority: 4 + # Third party library includes + - Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$" + Priority: 3 NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 diff --git a/cmake/FindMagicEnum.cmake b/cmake/FindMagicEnum.cmake index 0a77bd279fc..b595075cab5 100644 --- a/cmake/FindMagicEnum.cmake +++ b/cmake/FindMagicEnum.cmake @@ -1,6 +1,6 @@ include(FindPackageHandleStandardArgs) -find_path(MagicEnum_INCLUDE_DIR magic_enum.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/magic_enum/include) +find_path(MagicEnum_INCLUDE_DIR magic_enum/magic_enum.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/magic_enum/include) find_package_handle_standard_args(MagicEnum DEFAULT_MSG MagicEnum_INCLUDE_DIR) diff --git a/lib/magic_enum b/lib/magic_enum index e1a68e9dd3d..e55b9b54d5c 160000 --- a/lib/magic_enum +++ b/lib/magic_enum @@ -1 +1 @@ -Subproject commit e1a68e9dd3d2e9180b04c8aeacd4975db745e6b8 +Subproject commit e55b9b54d5cf61f8e117cafb17846d7d742dd3b4 diff --git a/mocks/.clang-format b/mocks/.clang-format index 7bae09f2ce3..0feaad9dc10 100644 --- a/mocks/.clang-format +++ b/mocks/.clang-format @@ -32,9 +32,6 @@ IncludeCategories: # Project includes - Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$' Priority: 1 - # Third party library includes - - Regex: '<[[:alnum:].]+/[a-zA-Z0-9\._\/-]+>' - Priority: 3 # Qt includes - Regex: '^$' Priority: 3 @@ -42,12 +39,12 @@ IncludeCategories: # LibCommuni includes - Regex: "^$" Priority: 3 - # Misc libraries - - Regex: '^<[a-zA-Z_0-9]+\.h(pp)?>$' - Priority: 3 # Standard library includes - Regex: "^<[a-zA-Z_]+>$" Priority: 4 + # Third party library includes + - Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$" + Priority: 3 NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 diff --git a/src/.clang-format b/src/.clang-format index 7bae09f2ce3..0feaad9dc10 100644 --- a/src/.clang-format +++ b/src/.clang-format @@ -32,9 +32,6 @@ IncludeCategories: # Project includes - Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$' Priority: 1 - # Third party library includes - - Regex: '<[[:alnum:].]+/[a-zA-Z0-9\._\/-]+>' - Priority: 3 # Qt includes - Regex: '^$' Priority: 3 @@ -42,12 +39,12 @@ IncludeCategories: # LibCommuni includes - Regex: "^$" Priority: 3 - # Misc libraries - - Regex: '^<[a-zA-Z_0-9]+\.h(pp)?>$' - Priority: 3 # Standard library includes - Regex: "^<[a-zA-Z_]+>$" Priority: 4 + # Third party library includes + - Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$" + Priority: 3 NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 diff --git a/src/Application.cpp b/src/Application.cpp index 0a2f8b4defe..d8a7a575469 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -546,9 +546,15 @@ void Application::initPubSub() msg.senderUserID, msg.senderUserLogin, senderDisplayName, senderColor}; postToThread([chan, action] { - const auto p = makeAutomodMessage(action); + const auto p = + makeAutomodMessage(action, chan->getName()); chan->addMessage(p.first); chan->addMessage(p.second); + + getApp()->twitch->automodChannel->addMessage( + p.first); + getApp()->twitch->automodChannel->addMessage( + p.second); }); } // "ALLOWED" and "DENIED" statuses remain unimplemented @@ -573,7 +579,7 @@ void Application::initPubSub() } postToThread([chan, action] { - const auto p = makeAutomodMessage(action); + const auto p = makeAutomodMessage(action, chan->getName()); chan->addMessage(p.first); chan->addMessage(p.second); }); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 269479d9eea..83f8dba5c7d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -633,6 +633,8 @@ set(SOURCE_FILES widgets/helper/SignalLabel.hpp widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.hpp + widgets/helper/TitlebarButtons.cpp + widgets/helper/TitlebarButtons.hpp widgets/listview/GenericItemDelegate.cpp widgets/listview/GenericItemDelegate.hpp diff --git a/src/common/Args.cpp b/src/common/Args.cpp index 2894c3f7bab..7bc48573c8f 100644 --- a/src/common/Args.cpp +++ b/src/common/Args.cpp @@ -38,6 +38,9 @@ Args::Args(const QApplication &app) "Attaches to the Console on windows, " "allowing you to see debug output."}); crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); + QCommandLineOption safeModeOption( + "safe-mode", "Starts Chatterino without loading Plugins and always " + "show the settings button."); parser.addOptions({ {{"V", "version"}, "Displays version information."}, @@ -45,6 +48,7 @@ Args::Args(const QApplication &app) parentWindowOption, parentWindowIdOption, verboseOption, + safeModeOption, }); parser.addOption(QCommandLineOption( {"c", "channels"}, @@ -89,6 +93,10 @@ Args::Args(const QApplication &app) this->parentWindowId = parser.value(parentWindowIdOption).toULongLong(); } + if (parser.isSet(safeModeOption)) + { + this->safeMode = true; + } } void Args::applyCustomChannelLayout(const QString &argValue) diff --git a/src/common/Args.hpp b/src/common/Args.hpp index 9fba4bdaf46..73144b7da9c 100644 --- a/src/common/Args.hpp +++ b/src/common/Args.hpp @@ -26,6 +26,7 @@ class Args bool dontLoadMainWindow{}; std::optional customChannelLayout; bool verbose{}; + bool safeMode{}; private: void applyCustomChannelLayout(const QString &argValue); diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index f0dc5d185c9..47ae99c1c2e 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -295,7 +295,8 @@ bool Channel::isWritable() const { using Type = Channel::Type; auto type = this->getType(); - return type != Type::TwitchMentions && type != Type::TwitchLive; + return type != Type::TwitchMentions && type != Type::TwitchLive && + type != Type::TwitchAutomod; } void Channel::sendMessage(const QString &message) @@ -330,7 +331,8 @@ bool Channel::isLive() const bool Channel::shouldIgnoreHighlights() const { - return this->type_ == Type::TwitchMentions || + return this->type_ == Type::TwitchAutomod || + this->type_ == Type::TwitchMentions || this->type_ == Type::TwitchWhispers; } diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 7fcb5636cbb..4fc686c06db 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -38,6 +38,7 @@ class Channel : public std::enable_shared_from_this TwitchWatching, TwitchMentions, TwitchLive, + TwitchAutomod, TwitchEnd, Irc, Misc diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 6c9d8ec4728..2f5a0cac436 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index 820916c365a..0a7a72d1695 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -30,7 +30,7 @@ namespace chatterino { enum class WindowType; struct SplitDescriptor { - // Twitch or mentions or watching or whispers or IRC + // Twitch or mentions or watching or live or automod or whispers or IRC QString type_; // Twitch Channel name or IRC channel name diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index 0fc85f97e79..ea618d99aa1 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -56,7 +56,7 @@ std::vector HotkeyController::shortcutsForCategory( { qCDebug(chatterinoHotkeys) << qPrintable(parent->objectName()) - << "Unimplemeneted hotkey action:" << hotkey->action() << "in " + << "Unimplemented hotkey action:" << hotkey->action() << "in" << hotkey->getCategory(); continue; } diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 6a75b774a35..f88f2a92836 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -4,7 +4,7 @@ # include # include -# include +# include # include # include diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index 3fdc8e4dca8..0e9a20d631f 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -4,7 +4,7 @@ # include "controllers/commands/CommandController.hpp" # include -# include +# include # include # include diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index e98a30720d0..9a3356e960b 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -2,6 +2,7 @@ # include "controllers/plugins/PluginController.hpp" # include "Application.hpp" +# include "common/Args.hpp" # include "common/QLogging.hpp" # include "controllers/commands/CommandContext.hpp" # include "controllers/commands/CommandController.hpp" @@ -194,12 +195,20 @@ void PluginController::openLibrariesFor(lua_State *L, void PluginController::load(const QFileInfo &index, const QDir &pluginDir, const PluginMeta &meta) { - lua_State *l = luaL_newstate(); - PluginController::openLibrariesFor(l, meta); - auto pluginName = pluginDir.dirName(); + lua_State *l = luaL_newstate(); auto plugin = std::make_unique(pluginName, l, meta, pluginDir); this->plugins_.insert({pluginName, std::move(plugin)}); + + if (getArgs().safeMode) + { + // This isn't done earlier to ensure the user can disable a misbehaving plugin + qCWarning(chatterinoLua) << "Skipping loading plugin " << meta.name + << " because safe mode is enabled."; + return; + } + PluginController::openLibrariesFor(l, meta); + if (!PluginController::isPluginEnabled(pluginName) || !getSettings()->pluginsEnabled) { diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index c34fe74e582..b605544b904 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -141,13 +141,14 @@ MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action) } std::pair makeAutomodMessage( - const AutomodAction &action) + const AutomodAction &action, const QString &channelName) { MessageBuilder builder, builder2; // // Builder for AutoMod message with explanation builder.message().loginName = "automod"; + builder.message().channelName = channelName; builder.message().flags.set(MessageFlag::PubSub); builder.message().flags.set(MessageFlag::Timeout); builder.message().flags.set(MessageFlag::AutoMod); @@ -193,6 +194,12 @@ std::pair makeAutomodMessage( // // Builder for offender's message + builder2.message().channelName = channelName; + builder2 + .emplace("#" + channelName, + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, channelName}); builder2.emplace(); builder2.emplace(); builder2.message().loginName = action.target.login; diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 28874439b51..333d2346904 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -54,7 +54,7 @@ const ImageUploaderResultTag imageUploaderResultMessage{}; MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); std::pair makeAutomodMessage( - const AutomodAction &action); + const AutomodAction &action, const QString &channelName); MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); struct MessageParseArgs { diff --git a/src/providers/seventv/SeventvCosmetics.hpp b/src/providers/seventv/SeventvCosmetics.hpp index 0d521ac03a8..6302c51f45d 100644 --- a/src/providers/seventv/SeventvCosmetics.hpp +++ b/src/providers/seventv/SeventvCosmetics.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace chatterino::seventv { diff --git a/src/providers/seventv/eventapi/Message.hpp b/src/providers/seventv/eventapi/Message.hpp index 5dbc848b756..4227f683990 100644 --- a/src/providers/seventv/eventapi/Message.hpp +++ b/src/providers/seventv/eventapi/Message.hpp @@ -2,7 +2,7 @@ #include "providers/seventv/eventapi/Subscription.hpp" -#include +#include #include #include #include diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp index 1a36811a56b..65cf0354443 100644 --- a/src/providers/seventv/eventapi/Subscription.hpp +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index fb266b692fa..270f1baa6d5 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -43,6 +43,7 @@ TwitchIrcServer::TwitchIrcServer() : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) , liveChannel(new Channel("/live", Channel::Type::TwitchLive)) + , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) { this->initializeIrc(); @@ -273,6 +274,11 @@ std::shared_ptr TwitchIrcServer::getCustomChannel( return this->liveChannel; } + if (channelName == "/automod") + { + return this->automodChannel; + } + static auto getTimer = [](ChannelPtr channel, int msBetweenMessages, bool addInitialMessages) { if (addInitialMessages) @@ -384,6 +390,7 @@ void TwitchIrcServer::forEachChannelAndSpecialChannels( func(this->whispersChannel); func(this->mentionsChannel); func(this->liveChannel); + func(this->automodChannel); } std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index ee8de56611a..d28f51e686d 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -77,6 +77,7 @@ class TwitchIrcServer final : public AbstractIrcServer, const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; const ChannelPtr liveChannel; + const ChannelPtr automodChannel; IndirectChannel watchingChannel; PubSub *pubsub; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 9f9391f733b..13ad6e9b971 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -6,7 +6,7 @@ #include "common/QLogging.hpp" #include "util/CancellationToken.hpp" -#include +#include #include namespace { diff --git a/src/providers/twitch/pubsubmessages/AutoMod.hpp b/src/providers/twitch/pubsubmessages/AutoMod.hpp index 400169ac7be..9f40d39da67 100644 --- a/src/providers/twitch/pubsubmessages/AutoMod.hpp +++ b/src/providers/twitch/pubsubmessages/AutoMod.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include diff --git a/src/providers/twitch/pubsubmessages/Base.hpp b/src/providers/twitch/pubsubmessages/Base.hpp index ce190ee065d..fed0112e7e0 100644 --- a/src/providers/twitch/pubsubmessages/Base.hpp +++ b/src/providers/twitch/pubsubmessages/Base.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp index c5a3ffe88d5..be8d1bd68af 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp b/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp index 5f29673e3d3..e04019cb7cf 100644 --- a/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp +++ b/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/providers/twitch/pubsubmessages/Whisper.hpp b/src/providers/twitch/pubsubmessages/Whisper.hpp index ef96cd62b35..979cb6a1ebf 100644 --- a/src/providers/twitch/pubsubmessages/Whisper.hpp +++ b/src/providers/twitch/pubsubmessages/Whisper.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index a86fdbc5ac1..5174f2a3399 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -87,8 +87,7 @@ void WindowManager::showAccountSelectPopup(QPoint point) w->refresh(); - QPoint buttonPos = point; - w->move(buttonPos.x() - 30, buttonPos.y()); + w->moveTo(point - QPoint(30, 0), widgets::BoundsChecking::CursorPosition); w->show(); w->setFocus(); } @@ -604,6 +603,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) obj.insert("name", channel.get()->getName()); } break; + case Channel::Type::TwitchAutomod: { + obj.insert("type", "automod"); + } + break; case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); } @@ -677,6 +680,10 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { return app->twitch->liveChannel; } + else if (descriptor.type_ == "automod") + { + return app->twitch->automodChannel; + } else if (descriptor.type_ == "irc") { return Irc::instance().getOrAddChannel(descriptor.server_, diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index d73ec79e533..c6b36d11e23 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -29,6 +29,10 @@ LoggingChannel::LoggingChannel(const QString &_channelName, { this->subDirectory = "Live"; } + else if (channelName.startsWith("/automod")) + { + this->subDirectory = "AutoMod"; + } else { this->subDirectory = @@ -96,7 +100,8 @@ void LoggingChannel::addMessage(MessagePtr message) } QString str; - if (channelName.startsWith("/mentions")) + if (channelName.startsWith("/mentions") || + channelName.startsWith("/automod")) { str.append("#" + message->channelName + " "); } diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index 7d687b66732..b381af01ed8 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -24,7 +24,7 @@ void initUpdateButton(Button &button, globalPoint.setX(0); } - dialog->move(globalPoint); + dialog->moveTo(globalPoint, widgets::BoundsChecking::DesiredPosition); dialog->show(); dialog->raise(); diff --git a/src/util/WidgetHelpers.cpp b/src/util/WidgetHelpers.cpp index 7e68727603e..b5e6fa9a303 100644 --- a/src/util/WidgetHelpers.cpp +++ b/src/util/WidgetHelpers.cpp @@ -79,4 +79,17 @@ void moveWindowTo(QWidget *window, QPoint position, BoundsChecking mode) } } +void showAndMoveWindowTo(QWidget *window, QPoint position, BoundsChecking mode) +{ +#ifdef Q_OS_WINDOWS + window->show(); + + moveWindowTo(window, position, mode); +#else + moveWindowTo(window, position, mode); + + window->show(); +#endif +} + } // namespace chatterino::widgets diff --git a/src/util/WidgetHelpers.hpp b/src/util/WidgetHelpers.hpp index 7f57ad39383..b09e93d0b95 100644 --- a/src/util/WidgetHelpers.hpp +++ b/src/util/WidgetHelpers.hpp @@ -26,4 +26,14 @@ enum class BoundsChecking { void moveWindowTo(QWidget *window, QPoint position, BoundsChecking mode = BoundsChecking::DesiredPosition); +/// Moves the `window` to the (global) `position` +/// while doing bounds-checking according to `mode` to ensure the window stays on one screen. +/// Will also call show on the `window`, order is dependant on platform. +/// +/// @param window The window to move. +/// @param position The global position to move the window to. +/// @param mode The desired bounds checking. +void showAndMoveWindowTo(QWidget *window, QPoint position, + BoundsChecking mode = BoundsChecking::DesiredPosition); + } // namespace chatterino::widgets diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index ad12fe185b3..59abe0a4f4f 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -8,6 +8,7 @@ #include "util/PostToThread.hpp" #include "util/WindowsHelper.hpp" #include "widgets/helper/EffectLabel.hpp" +#include "widgets/helper/TitlebarButtons.hpp" #include "widgets/Label.hpp" #include "widgets/TooltipWidget.hpp" #include "widgets/Window.hpp" @@ -180,9 +181,8 @@ void BaseWindow::init() this->close(); }); - this->ui_.minButton = _minButton; - this->ui_.maxButton = _maxButton; - this->ui_.exitButton = _exitButton; + this->ui_.titlebarButtons = new TitleBarButtons( + this, _minButton, _maxButton, _exitButton); this->ui_.buttons.push_back(_minButton); this->ui_.buttons.push_back(_maxButton); @@ -474,12 +474,9 @@ void BaseWindow::changeEvent(QEvent *) } #ifdef USEWINSDK - if (this->ui_.maxButton) + if (this->ui_.titlebarButtons) { - this->ui_.maxButton->setButtonStyle( - this->windowState() & Qt::WindowMaximized - ? TitleBarButtonStyle::Unmaximize - : TitleBarButtonStyle::Maximize); + this->ui_.titlebarButtons->updateMaxButton(); } if (this->isVisible() && this->hasCustomWindowFrame()) @@ -508,6 +505,11 @@ void BaseWindow::moveTo(QPoint point, widgets::BoundsChecking mode) widgets::moveWindowTo(this, point, mode); } +void BaseWindow::showAndMoveTo(QPoint point, widgets::BoundsChecking mode) +{ + widgets::showAndMoveWindowTo(this, point, mode); +} + void BaseWindow::resizeEvent(QResizeEvent *) { // Queue up save because: Window resized @@ -559,6 +561,12 @@ void BaseWindow::closeEvent(QCloseEvent *) void BaseWindow::showEvent(QShowEvent *) { +#ifdef Q_OS_WIN + if (this->flags_.has(BoundsCheckOnShow)) + { + this->moveTo(this->pos(), widgets::BoundsChecking::CursorPosition); + } +#endif } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -574,6 +582,11 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, bool returnValue = false; + auto isHoveringTitlebarButton = [&]() { + auto ht = msg->wParam; + return ht == HTMAXBUTTON || ht == HTMINBUTTON || ht == HTCLOSE; + }; + switch (msg->message) { case WM_DPICHANGED: @@ -601,6 +614,91 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, returnValue = this->handleNCHITTEST(msg, result); break; + case WM_NCMOUSEHOVER: + case WM_NCMOUSEMOVE: { + // WM_NCMOUSEMOVE/WM_NCMOUSEHOVER gets sent when the mouse is + // moving/hovering in the non-client area + // - (mostly) the edges and the titlebar. + // We only need to handle the event for the titlebar buttons, + // as Qt doesn't create mouse events for these events. + if (!this->ui_.titlebarButtons) + { + // we don't consume the event if we don't have custom buttons + break; + } + + if (isHoveringTitlebarButton()) + { + *result = 0; + returnValue = true; + long x = GET_X_LPARAM(msg->lParam); + long y = GET_Y_LPARAM(msg->lParam); + + RECT winrect; + GetWindowRect(HWND(winId()), &winrect); + QPoint globalPos(x, y); + this->ui_.titlebarButtons->hover(msg->wParam, globalPos); + this->lastEventWasNcMouseMove_ = true; + } + else + { + this->ui_.titlebarButtons->leave(); + } + } + break; + + case WM_MOUSEMOVE: { + if (!this->lastEventWasNcMouseMove_) + { + break; + } + this->lastEventWasNcMouseMove_ = false; + // Windows doesn't send WM_NCMOUSELEAVE in some cases, + // so the buttons show as hovered even though they're not hovered. + [[fallthrough]]; + } + case WM_NCMOUSELEAVE: { + // WM_NCMOUSELEAVE gets sent when the mouse leaves any + // non-client area. In case we have titlebar buttons, + // we want to ensure they're deselected. + if (this->ui_.titlebarButtons) + { + this->ui_.titlebarButtons->leave(); + } + } + break; + + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: { + // WM_NCLBUTTON{DOWN, UP} gets called when the left mouse button + // was pressed in a non-client area. + // We simulate a mouse down/up event for the titlebar buttons + // as Qt doesn't create an event in that case. + if (!this->ui_.titlebarButtons || !isHoveringTitlebarButton()) + { + break; + } + returnValue = true; + *result = 0; + + auto ht = msg->wParam; + long x = GET_X_LPARAM(msg->lParam); + long y = GET_Y_LPARAM(msg->lParam); + + RECT winrect; + GetWindowRect(HWND(winId()), &winrect); + QPoint globalPos(x, y); + if (msg->message == WM_NCLBUTTONDOWN) + { + this->ui_.titlebarButtons->mousePress(ht, globalPos); + } + else + { + this->ui_.titlebarButtons->mouseRelease(ht, globalPos); + } + } + break; + default: return QWidget::nativeEvent(eventType, message, result); } @@ -657,29 +755,21 @@ void BaseWindow::calcButtonsSizes() return; } - if (this->frameless_) + if (this->frameless_ || !this->ui_.titlebarButtons) { return; } - if ((this->width() / this->scale()) < 300) +#ifdef USEWINSDK + if ((static_cast(this->width()) / this->scale()) < 300) { - if (this->ui_.minButton) - this->ui_.minButton->setScaleIndependantSize(30, 30); - if (this->ui_.maxButton) - this->ui_.maxButton->setScaleIndependantSize(30, 30); - if (this->ui_.exitButton) - this->ui_.exitButton->setScaleIndependantSize(30, 30); + this->ui_.titlebarButtons->setSmallSize(); } else { - if (this->ui_.minButton) - this->ui_.minButton->setScaleIndependantSize(46, 30); - if (this->ui_.maxButton) - this->ui_.maxButton->setScaleIndependantSize(46, 30); - if (this->ui_.exitButton) - this->ui_.exitButton->setScaleIndependantSize(46, 30); + this->ui_.titlebarButtons->setRegularSize(); } +#endif } void BaseWindow::drawCustomWindowFrame(QPainter &painter) @@ -932,32 +1022,55 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (*result == 0) { - bool client = false; - // Check the main layout first, as it's the largest area if (this->ui_.layoutBase->geometry().contains(point)) { - client = true; + *result = HTCLIENT; } // Check the titlebar buttons - if (!client && this->ui_.titlebarBox->geometry().contains(point)) + if (*result == 0 && + this->ui_.titlebarBox->geometry().contains(point)) { - for (QWidget *widget : this->ui_.buttons) + for (const auto *widget : this->ui_.buttons) { - if (widget->isVisible() && - widget->geometry().contains(point)) + if (!widget->isVisible() || + !widget->geometry().contains(point)) { - client = true; + continue; } + + if (const auto *btn = + dynamic_cast(widget)) + { + switch (btn->getButtonStyle()) + { + case TitleBarButtonStyle::Minimize: { + *result = HTMINBUTTON; + break; + } + case TitleBarButtonStyle::Unmaximize: + case TitleBarButtonStyle::Maximize: { + *result = HTMAXBUTTON; + break; + } + case TitleBarButtonStyle::Close: { + *result = HTCLOSE; + break; + } + default: { + *result = HTCLIENT; + break; + } + } + break; + } + *result = HTCLIENT; + break; } } - if (client) - { - *result = HTCLIENT; - } - else + if (*result == 0) { *result = HTCAPTION; } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 197618657ae..d55b5bd6d16 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -18,6 +18,7 @@ namespace chatterino { class Button; class EffectLabel; class TitleBarButton; +class TitleBarButtons; enum class TitleBarButtonStyle; class BaseWindow : public BaseWidget @@ -27,14 +28,15 @@ class BaseWindow : public BaseWidget public: enum Flags { None = 0, - EnableCustomFrame = 1, - Frameless = 2, - TopMost = 4, - DisableCustomScaling = 8, - FramelessDraggable = 16, - DontFocus = 32, - Dialog = 64, - DisableLayoutSave = 128, + EnableCustomFrame = 1 << 0, + Frameless = 1 << 1, + TopMost = 1 << 2, + DisableCustomScaling = 1 << 3, + FramelessDraggable = 1 << 4, + DontFocus = 1 << 5, + Dialog = 1 << 6, + DisableLayoutSave = 1 << 7, + BoundsCheckOnShow = 1 << 8, }; enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; @@ -57,6 +59,12 @@ class BaseWindow : public BaseWidget void moveTo(QPoint point, widgets::BoundsChecking mode); + /** + * Moves the window to the given point and does bounds checking according to `mode` + * Depending on the platform, either the move or the show will take place first + **/ + void showAndMoveTo(QPoint point, widgets::BoundsChecking mode); + float scale() const override; float qtFontScale() const; @@ -128,9 +136,7 @@ class BaseWindow : public BaseWidget QLayout *windowLayout = nullptr; QHBoxLayout *titlebarBox = nullptr; QWidget *titleLabel = nullptr; - TitleBarButton *minButton = nullptr; - TitleBarButton *maxButton = nullptr; - TitleBarButton *exitButton = nullptr; + TitleBarButtons *titlebarButtons = nullptr; QWidget *layoutBase = nullptr; std::vector