From fb6beb4acabeb04464f27972f232468098883b65 Mon Sep 17 00:00:00 2001 From: DatGuy1 Date: Tue, 26 Mar 2024 20:51:16 +0200 Subject: [PATCH 001/105] fix: add trailing space if deletion link included in image upload response (#5269) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c039db91a1..5245db53b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) - Bugfix: Detect when OBS is running on MacOS. (#5260) +- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) - 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) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index af5dadcf707..cc71558f65e 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -543,7 +543,7 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/, // This also ensures that the LinkResolver doesn't get these links. addText(imageLink, MessageColor::Link) ->setLink({Link::Url, imageLink}) - ->setTrailingSpace(false); + ->setTrailingSpace(!deletionLink.isEmpty()); if (!deletionLink.isEmpty()) { From 337ae52a5d581b8b9d523f063ea9b8009a2b081e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:27:45 +0000 Subject: [PATCH 002/105] chore(deps): bump ZedThree/clang-tidy-review from 0.17.2 to 0.17.3 (#5271) Bumps [ZedThree/clang-tidy-review](https://github.com/zedthree/clang-tidy-review) from 0.17.2 to 0.17.3. - [Release notes](https://github.com/zedthree/clang-tidy-review/releases) - [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md) - [Commits](https://github.com/zedthree/clang-tidy-review/compare/v0.17.2...v0.17.3) --- updated-dependencies: - dependency-name: ZedThree/clang-tidy-review dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/clang-tidy.yml | 4 ++-- .github/workflows/post-clang-tidy-review.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 0f072a36442..b522bba5d3e 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -119,7 +119,7 @@ jobs: - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.2 + uses: ZedThree/clang-tidy-review@v0.17.3 with: build_dir: build-clang-tidy config_file: ".clang-tidy" @@ -145,4 +145,4 @@ jobs: libbenchmark-dev - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.2 + uses: ZedThree/clang-tidy-review/upload@v0.17.3 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index e22b264f502..2f9b6b3d9fa 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -14,6 +14,6 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.2 + - uses: ZedThree/clang-tidy-review/post@v0.17.3 with: lgtm_comment_body: "" From 515a92d6f7643c6af205e830e5964edfa98adffa Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:59:57 -0400 Subject: [PATCH 003/105] Prepare changelog for v2.5.0 release (#5264) --- CHANGELOG.md | 120 +++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5245db53b29..a8182382c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,119 +2,119 @@ ## Unversioned -- 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, #5026) -- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060) -- 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) -- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847) -- Minor: Allow running `/ban`, `/timeout`, `/unban`, and `/untimeout` on User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957) -- Minor: The `/usercard` command now accepts user ids. (#4934) -- Minor: Add menu actions to reply directly to a message or the original thread root. (#4923) -- 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) -- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - right now only supporting bot badges for your chat bots. (#5119) -- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) -- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047) -- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) +- Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) +- Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) +- Major: Moderators can now see restricted chat messages and suspicious treatment updates. (#5056, #5060) +- Minor: Migrated to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) +- Minor: Moderation commands such as `/ban`, `/timeout`, `/unban`, and `/untimeout` can now be used via User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957) +- Minor: The `/usercard` command now accepts user ids. (`/usercard id:22484632`) (#4934) +- Minor: Added menu actions to reply directly to a message or the original thread root. (#4923) +- Minor: The `/reply` command now replies to the latest message from the user. Due to this change, the message you intended to reply to is now shown in the reply context, instead of the first message in a thread. (#4919) - Minor: The chatter list button is now hidden if you don't have moderator privileges. (#5245) +- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237) +- Minor: Allowed theming of tab live and rerun indicators. (#5188) +- Minor: The _Restart on crash_ setting works again on Windows. (#5012) +- Minor: Added an option to use new experimental smarter emote completion. (#4987) +- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - currently only supports bot badges for your chat bots. (#5119) +- Minor: Added support to send /announce[color] commands. Colored announcements only appear with the chosen color in Twitch chat. (#5250) - Minor: The whisper highlight color can now be configured through the settings. (#5053) - Minor: Added an option to always include the broadcaster in user completions. This is enabled by default. (#5193, #5244) -- Minor: Added missing periods at various moderator messages and commands. (#5061) -- Minor: Improved color selection and display. (#5057) -- Minor: Improved Streamlink documentation in the settings dialog. (#5076) -- Minor: Normalized the input padding between light & dark themes. (#5095) -- Minor: Add `--activate ` (or `-a`) command line option to activate or add a Twitch channel. (#5111) -- Minor: Chatters from recent-messages are now added to autocompletion. (#5116) -- Minor: Added a _System_ theme that updates according to the system's color scheme (requires Qt 6.5). (#5118) -- Minor: Added icons for newer versions of macOS. (#5148) -- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197) +- Minor: Added a warning message if you have multiple commands with the same trigger. (#4322) +- Minor: Chatters from message history are now added to autocompletion. (#5116) - Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130) -- Minor: Add a new Channel API for experimental plugins feature. (#5141, #5184, #5187) +- Minor: Added `--activate ` (or `-a`) command line option to focus or add a certain Twitch channel on startup. (#5111) +- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197) - Minor: Added the ability to change the top-most status of a window regardless of the _Always on top_ setting (right click the notebook). (#5135) -- Minor: Introduce `c2.later()` function to Lua API. (#5154) -- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237) -- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Added the ability to show AutoMod caught messages in mentions. (#5215) - Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215) -- Minor: Allow theming of tab live and rerun indicators. (#5188) +- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) +- Minor: Added icons for newer versions of macOS. (#5148) +- Minor: Improved color selection and display. (#5057) +- Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118) +- Minor: Normalized the input padding between light & dark themes. (#5095) +- Minor: The account switcher is now styled to match your theme. (#4817) - Minor: Added a fallback theme field to custom themes that will be used in case the custom theme does not contain a color Chatterino needs. If no fallback theme is specified, we'll pull the color from the included Dark or Light theme. (#5198) +- Minor: Added a new completion API for experimental plugins feature. (#5000, #5047) +- Minor: Added a new Channel API for experimental plugins feature. (#5141, #5184, #5187) +- Minor: Introduce `c2.later()` function to Lua API. (#5154) +- Minor: Added `--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) +- Minor: Added wrappers for Lua `io` library for experimental plugins feature. (#5231) +- Minor: Added permissions to experimental plugins feature. (#5231) +- Minor: Added missing periods at various moderator messages and commands. (#5061) +- Minor: Improved Streamlink documentation in the settings dialog. (#5076) +- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847) +- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) +- Minor: Added an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) +- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) - Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) -- Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231) -- Minor: Add permissions to experimental plugins feature. (#5231) -- Minor: Added warning message if you have multiple commands with the same trigger. (#4322) -- Minor: Add support to send /announce[color] commands. (#5250) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) +- Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - 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) - Bugfix: Fixed a performance issue when displaying replies to certain messages. (#4807) - Bugfix: Fixed an issue where certain parts of the split input wouldn't focus the split when clicked. (#4958) -- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) -- Bugfix: Fixed `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) -- Bugfix: Fixed Usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) +- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172)\ +- Bugfix: User text input within watch streak notices now correctly shows up. (#5029) - Bugfix: Fixed selection of tabs after closing a tab when using "Live Tabs Only". (#4770) -- Bugfix: Fixed input in reply thread popup losing focus when dragging. (#4815) -- Bugfix: Fixed the Quick Switcher (CTRL+K) from sometimes showing up on the wrong window. (#4819) +- Bugfix: Fixed input in the reply thread popup losing focus when dragging said window. (#4815) +- Bugfix: Fixed the Quick Switcher (CTRL+K) sometimes showing up on the wrong window. (#4819) - Bugfix: Fixed the font switcher not remembering what font you had previously selected. (#5224) - Bugfix: Fixed too much text being copied when copying chat messages. (#4812, #4830, #4839) -- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855) -- Bugfix: Fixed empty page being added when showing out of bounds dialog. (#4849) -- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained text input. (#5117) - Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873) +- Bugfix: Fixed an issue where Streamer Mode did not detect that OBS was running on MacOS. (#5260) +- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) +- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) +- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855) +- Bugfix: Fixed an empty page being added when showing the out of bounds dialog. (#4849) +- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained user text input. (#5117) - Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875, #4977, #5174) -- Bugfix: Fixed the input completion popup from disappearing when clicking on it on Windows and macOS. (#4876) +- Bugfix: Fixed the input completion popup sometimes disappearing when clicking on it on Windows and macOS. (#4876) - Bugfix: Fixed Twitch badges not loading correctly in the badge highlighting setting page. (#5223) - Bugfix: Fixed double-click text selection moving its position with each new message. (#4898) - Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899) - Bugfix: Fixed headers of tables in the settings switching to bold text when selected. (#4913) -- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172) - Bugfix: Fixed tooltips appearing too large and/or away from the cursor. (#4920) -- Bugfix: Fixed a crash when clicking `More messages below` button in a usercard and closing it quickly. (#4933) - Bugfix: Fixed thread popup window missing messages for nested threads. (#4923) - Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949) -- Bugfix: Fixed triple click on message also selecting moderation buttons. (#4961) -- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126) +- Bugfix: Fixed triple-click on message also selecting moderation buttons. (#4961) - Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110) - Bugfix: Fixed emotes being reloaded when pressing "Cancel" in the settings dialog, causing a slowdown. (#5240) +- Bugfix: Fixed double-click selection not correctly selecting words that were split onto multiple lines. (#5243) - Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126) -- Bugfix: Fixed double-click selection not selecting words that were split onto multiple lines correctly. (#5243) +- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126) - 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 an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011) -- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972) +- Bugfix: Fixed a rare crash with the Image Uploader when closing a split right after starting an upload. (#4971) +- Bugfix: Fixed an issue on macOS where the Image Uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011) +- Bugfix: The usercard button is now hidden in the User Info Popup when in special channels. (#4972) - Bugfix: Fixed support for Windows 11 Snap layouts. (#4994, #5175) - Bugfix: Fixed some windows appearing between screens. (#4797) +- Bugfix: Fixed a crash that could occur when clicking `More messages below` button in a usercard and closing it quickly. (#4933) - Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051) - Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051) - Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040) - Bugfix: Fixes to section deletion in text input fields. (#5013) -- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) -- Bugfix: Show user text input within watch streak notices. (#5029) - Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052) -- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077) - Bugfix: Fixed an issue where you had to click the `reply` button twice if you already had that users @ in your input box. (#5173) - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) - Bugfix: Fixed link info not updating without moving the cursor. (#5178) - Bugfix: Fixed an upload sometimes failing when copying an image from a browser if it contained extra properties. (#5156) - Bugfix: Fixed tooltips getting out of bounds when loading images. (#5186) -- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) -- Bugfix: Detect when OBS is running on MacOS. (#5260) -- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) -- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) +- Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) +- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) +- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) +- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) - 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) From b6d75fd8672d7b00a3675b483e0f71e133788888 Mon Sep 17 00:00:00 2001 From: Maverick Date: Fri, 29 Mar 2024 20:50:43 +0100 Subject: [PATCH 004/105] feat: add more items in macOS menu bar (#5266) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/common/Common.hpp | 4 +++ src/widgets/Window.cpp | 37 ++++++++++++++++++++++++ src/widgets/dialogs/SettingsDialog.cpp | 7 ++++- src/widgets/dialogs/SettingsDialog.hpp | 1 + src/widgets/helper/SettingsDialogTab.hpp | 1 + src/widgets/settingspages/AboutPage.cpp | 3 +- 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8182382c46..cd6df89d280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215) - Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) - Minor: Added icons for newer versions of macOS. (#5148) +- Minor: Added more menu items in macOS menu bar. (#5266) - Minor: Improved color selection and display. (#5057) - Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118) - Minor: Normalized the input padding between light & dark themes. (#5095) diff --git a/src/common/Common.hpp b/src/common/Common.hpp index b0315a8aaf2..35b8efb1cc3 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -8,6 +8,10 @@ #include #include +#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com" +#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z" +#define LINK_CHATTERINO_SOURCE "https://github.com/Chatterino/chatterino2" + namespace chatterino { enum class HighlightState { diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 8b3cea43033..7106f872fcf 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/Args.hpp" +#include "common/Common.hpp" #include "common/Credentials.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" @@ -702,6 +703,14 @@ void Window::addMenuBar() // First menu. QMenu *menu = mainMenu->addMenu(QString()); + + // About button that shows the About tab in the Settings Dialog. + QAction *about = menu->addAction(QString()); + about->setMenuRole(QAction::AboutRole); + connect(about, &QAction::triggered, this, [this] { + SettingsDialog::showDialog(this, SettingsDialogPreference::About); + }); + QAction *prefs = menu->addAction(QString()); prefs->setMenuRole(QAction::PreferencesRole); connect(prefs, &QAction::triggered, this, [this] { @@ -711,6 +720,13 @@ void Window::addMenuBar() // Window menu. QMenu *windowMenu = mainMenu->addMenu(QString("Window")); + // Window->Minimize item + QAction *minimizeWindow = windowMenu->addAction(QString("Minimize")); + minimizeWindow->setShortcuts({QKeySequence("Meta+M")}); + connect(minimizeWindow, &QAction::triggered, this, [this] { + this->setWindowState(Qt::WindowMinimized); + }); + QAction *nextTab = windowMenu->addAction(QString("Select next tab")); nextTab->setShortcuts({QKeySequence("Meta+Tab")}); connect(nextTab, &QAction::triggered, this, [this] { @@ -722,6 +738,27 @@ void Window::addMenuBar() connect(prevTab, &QAction::triggered, this, [this] { this->notebook_->selectPreviousTab(); }); + + // Help menu. + QMenu *helpMenu = mainMenu->addMenu(QString("Help")); + + // Help->Chatterino Wiki item + QAction *helpWiki = helpMenu->addAction(QString("Chatterino Wiki")); + connect(helpWiki, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_WIKI)); + }); + + // Help->Chatterino Github + QAction *helpGithub = helpMenu->addAction(QString("Chatterino GitHub")); + connect(helpGithub, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_SOURCE)); + }); + + // Help->Chatterino Discord + QAction *helpDiscord = helpMenu->addAction(QString("Chatterino Discord")); + connect(helpDiscord, &QAction::triggered, this, []() { + QDesktopServices::openUrl(QUrl(LINK_CHATTERINO_DISCORD)); + }); } void Window::onAccountSelected() diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 49cdf8e3539..62d459e222f 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -249,7 +249,7 @@ void SettingsDialog::addTabs() this->addTab([]{return new PluginsPage;}, "Plugins", ":/settings/plugins.svg"); #endif this->ui_.tabContainer->addStretch(1); - this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId(), Qt::AlignBottom); + this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId::About, Qt::AlignBottom); // clang-format on } @@ -366,6 +366,11 @@ void SettingsDialog::showDialog(QWidget *parent, } break; + case SettingsDialogPreference::About: { + instance->selectTab(SettingsTabId::About); + } + break; + default:; } diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index e227223de96..6c32e0ccbc3 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -30,6 +30,7 @@ enum class SettingsDialogPreference { StreamerMode, Accounts, ModerationActions, + About, }; class SettingsDialog : public BaseWindow diff --git a/src/widgets/helper/SettingsDialogTab.hpp b/src/widgets/helper/SettingsDialogTab.hpp index 97a1ad51d6d..0c60688b2aa 100644 --- a/src/widgets/helper/SettingsDialogTab.hpp +++ b/src/widgets/helper/SettingsDialogTab.hpp @@ -18,6 +18,7 @@ enum class SettingsTabId { General, Accounts, Moderation, + About, }; class SettingsDialogTab : public BaseWidget diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 471b6d03b25..78597c5fca3 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -1,5 +1,6 @@ #include "AboutPage.hpp" +#include "common/Common.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" @@ -18,10 +19,8 @@ #define PIXMAP_WIDTH 500 -#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com" #define LINK_DONATE "https://streamelements.com/fourtf/tip" #define LINK_CHATTERINO_FEATURES "https://chatterino.com/#features" -#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z" namespace chatterino { From 9583a10b88c191d6dbde6e3b08719d62118fed1f Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:24:09 +0100 Subject: [PATCH 005/105] fix(helix-chat): show better error messages (#5276) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchIrcServer.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6df89d280..37080a27be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,7 +186,7 @@ - Dev: Added signal to invalidate paint buffers of channel views without forcing a relayout. (#5123) - Dev: Specialize `Atomic>` if underlying standard library supports it. (#5133) - Dev: Added the `developer_name` field to the Linux AppData specification. (#5138) -- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200) +- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200, #5276) - Dev: Added estimation for image sizes to avoid layout shifts. (#5192) - Dev: Added the `launachable` entry to Linux AppData. (#5210) - Dev: Cleaned up and optimized resources. (#5222) diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 9a71c89ac77..f591e8f310e 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -39,9 +39,17 @@ const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; void sendHelixMessage(const std::shared_ptr &channel, const QString &message, const QString &replyParentId = {}) { + auto broadcasterID = channel->roomId(); + if (broadcasterID.isEmpty()) + { + channel->addMessage(makeSystemMessage( + "Sending messages in this channel isn't possible.")); + return; + } + getHelix()->sendChatMessage( { - .broadcasterID = channel->roomId(), + .broadcasterID = broadcasterID, .senderID = getIApp()->getAccounts()->twitch.getCurrent()->getUserId(), .message = message, @@ -68,13 +76,18 @@ void sendHelixMessage(const std::shared_ptr &channel, }(); chan->addMessage(errorMessage); }, - [weak = std::weak_ptr(channel)](auto error, const auto &message) { + [weak = std::weak_ptr(channel)](auto error, auto message) { auto chan = weak.lock(); if (!chan) { return; } + if (message.isEmpty()) + { + message = "(empty message)"; + } + using Error = decltype(error); auto errorMessage = [&]() -> QString { From 84e641d5892af7d8946acd015922006c0966bac6 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:25:11 +0100 Subject: [PATCH 006/105] ci: run clang-tidy with Qt 6 and update action (#5273) --- .CI/setup-clang-tidy.sh | 34 ++++++ .github/workflows/clang-tidy.yml | 113 +++---------------- .github/workflows/post-clang-tidy-review.yml | 5 +- CHANGELOG.md | 1 + 4 files changed, 53 insertions(+), 100 deletions(-) create mode 100755 .CI/setup-clang-tidy.sh diff --git a/.CI/setup-clang-tidy.sh b/.CI/setup-clang-tidy.sh new file mode 100755 index 00000000000..4884285eb8e --- /dev/null +++ b/.CI/setup-clang-tidy.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -ev; + +# aqt installs into .qtinstall/Qt//gcc_64 +# This is doing the same as jurplel/install-qt-action +# See https://github.com/jurplel/install-qt-action/blob/74ca8cd6681420fc8894aed264644c7a76d7c8cb/action/src/main.ts#L52-L74 +qtpath=$(echo .qtinstall/Qt/[0-9]*/*/bin/qmake | sed -e s:/bin/qmake$::) +export LD_LIBRARY_PATH="$qtpath/lib" +export QT_ROOT_DIR=$qtpath +export QT_PLUGIN_PATH="$qtpath/plugins" +export PATH="$PATH:$(realpath "$qtpath/bin")" +export Qt6_DIR="$(realpath "$qtpath")" + +cmake -S. -Bbuild-clang-tidy \ + -DCMAKE_BUILD_TYPE=Debug \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO=Off \ + -DCHATTERINO_PLUGINS=On \ + -DBUILD_WITH_QT6=On \ + -DBUILD_TESTS=On \ + -DBUILD_BENCHMARKS=On + +# Run MOC and UIC +# This will compile the dependencies +# Get the targets using `ninja -t targets | grep autogen` +cmake --build build-clang-tidy --parallel -t \ + Core_autogen \ + LibCommuni_autogen \ + Model_autogen \ + Util_autogen \ + chatterino-lib_autogen diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index b522bba5d3e..cf47eacaf11 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -8,60 +8,25 @@ concurrency: group: clang-tidy-${{ github.ref }} cancel-in-progress: true -env: - CHATTERINO_REQUIRE_CLEAN_GIT: On - C2_BUILD_WITH_QT6: Off - jobs: - build: + review: name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" runs-on: ${{ matrix.os }} strategy: matrix: include: - # Ubuntu 22.04, Qt 5.15 + # Ubuntu 22.04, Qt 6.6 - os: ubuntu-22.04 - qt-version: 5.15.2 - plugins: false + qt-version: 6.6.2 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 @@ -70,79 +35,31 @@ jobs: 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 + dir: ${{ github.workspace }}/.qtinstall + set-env: false - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.3 + uses: ZedThree/clang-tidy-review@v0.18.0 with: build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true exclude: "lib/*,tools/crash-handler/*" 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 + ./.CI/setup-clang-tidy.sh 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 + libbenchmark-dev, + build-essential, + libgl1-mesa-dev, libgstreamer-gl1.0-0, libpulse-dev, + libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, + libxcb-render-util0, libxcb-render0, libxcb-shape0, libxcb-shm0, libxcb-sync1, + libxcb-util1, libxcb-xfixes0, libxcb-xinerama0, libxcb1, libxkbcommon-dev, + libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0 - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.3 + uses: ZedThree/clang-tidy-review/upload@v0.18.0 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index 2f9b6b3d9fa..6c39a93a7ea 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -8,12 +8,13 @@ on: - completed jobs: - build: + post: runs-on: ubuntu-latest # Only when a build succeeds if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.3 + - uses: ZedThree/clang-tidy-review/post@v0.18.0 with: lgtm_comment_body: "" + num_comments_as_exitcode: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 37080a27be6..b8287d63a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,7 @@ - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) +- Dev: `clang-tidy` CI now uses Qt 6. (#5273) ## 2.4.6 From 09b2c53383af4e4e35f402a909573675d51e09ad Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:56:51 +0100 Subject: [PATCH 007/105] fix: rerender when unpausing (#5265) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8287d63a05..1ce0f4f60fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) +- Bugfix: Fixed pause indicator not disappearing in some cases. (#5265) - Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 1454b4999de..9c45c6d9ab1 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -536,6 +536,8 @@ void ChannelView::updatePauses() this->pauseScrollMaximumOffset_ = 0; this->queueLayout(); + // make sure we re-render + this->update(); } else if (std::any_of(this->pauses_.begin(), this->pauses_.end(), [](auto &&value) { @@ -560,8 +562,9 @@ void ChannelView::updatePauses() { /// Start the timer this->pauseEnd_ = pauseEnd; - this->pauseTimer_.start( - duration_cast(pauseEnd - SteadyClock::now())); + auto duration = + duration_cast(pauseEnd - SteadyClock::now()); + this->pauseTimer_.start(std::max(duration, 0ms)); } } } From 69bdac9936cd981679a8f5d2bf15b7873292b83f Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 12:28:49 +0100 Subject: [PATCH 008/105] Add `reward.cost` `reward.id`, `reward.title` filter variables (#5275) --- CHANGELOG.md | 1 + src/controllers/filters/lang/Filter.cpp | 12 ++++++++++++ src/controllers/filters/lang/Filter.hpp | 3 +++ src/controllers/filters/lang/Tokenizer.hpp | 6 +++++- src/messages/Message.hpp | 3 +++ src/providers/twitch/TwitchMessageBuilder.cpp | 2 ++ src/widgets/helper/ChannelView.cpp | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce0f4f60fa..b82b51b8a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) +- Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 7ae61991a90..ef0cfd15ce2 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -120,6 +120,18 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) vars["channel.live"] = false; } } + if (m->reward != nullptr) + { + vars["reward.title"] = m->reward->title; + vars["reward.cost"] = m->reward->cost; + vars["reward.id"] = m->reward->id; + } + else + { + vars["reward.title"] = ""; + vars["reward.cost"] = -1; + vars["reward.id"] = ""; + } return vars; } diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp index c8afbd76916..01d7a765e9d 100644 --- a/src/controllers/filters/lang/Filter.hpp +++ b/src/controllers/filters/lang/Filter.hpp @@ -48,6 +48,9 @@ static const QMap MESSAGE_TYPING_CONTEXT = { {"flags.monitored", Type::Bool}, {"message.content", Type::String}, {"message.length", Type::Int}, + {"reward.title", Type::String}, + {"reward.cost", Type::Int}, + {"reward.id", Type::String}, }; ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); diff --git a/src/controllers/filters/lang/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp index 2fbc5fd9536..6ca9d373ceb 100644 --- a/src/controllers/filters/lang/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -35,7 +35,11 @@ static const QMap validIdentifiersMap = { {"flags.restricted", "restricted message?"}, {"flags.monitored", "monitored message?"}, {"message.content", "message text"}, - {"message.length", "message length"}}; + {"message.length", "message length"}, + {"reward.title", "point reward title"}, + {"reward.cost", "point reward cost"}, + {"reward.id", "point reward id"}, +}; // clang-format off static const QRegularExpression tokenRegex( diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index b9e0b2321ec..bdbe120ddaa 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/FlagsEnum.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "util/QStringHash.hpp" #include @@ -107,6 +108,8 @@ struct Message { std::vector> elements; ScrollbarHighlight getScrollBarHighlight() const; + + std::shared_ptr reward = nullptr; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 88c0f671e74..524d0375de5 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1625,6 +1625,8 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( builder->message().messageText = textList.join(" "); builder->message().searchText = textList.join(" "); builder->message().loginName = reward.user.login; + + builder->message().reward = std::make_shared(reward); } void TwitchMessageBuilder::liveMessage(const QString &channelName, diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 9c45c6d9ab1..2795e5ffe7b 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -268,6 +268,20 @@ void addHiddenContextMenuItems(QMenu *menu, jsonObject["searchText"] = message->searchText; jsonObject["messageText"] = message->messageText; jsonObject["flags"] = qmagicenum::enumFlagsName(message->flags.value()); + if (message->reward) + { + QJsonObject reward; + reward["id"] = message->reward->id; + reward["title"] = message->reward->title; + reward["cost"] = message->reward->cost; + reward["isUserInputRequired"] = + message->reward->isUserInputRequired; + jsonObject["reward"] = reward; + } + else + { + jsonObject["reward"] = QJsonValue(); + } jsonDocument.setObject(jsonObject); From 2f534dc6dabe84c002ca4e54325a779a596980e9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 30 Mar 2024 14:24:47 +0100 Subject: [PATCH 009/105] fix: override broken base sizes & scales for some Twitch emotes (#5279) --- CHANGELOG.md | 1 + src/providers/twitch/TwitchEmotes.cpp | 407 +++++++++++++++++++++++++- src/providers/twitch/TwitchEmotes.hpp | 1 - 3 files changed, 398 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82b51b8a47..b3b0c05006d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279) - Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 4c87e472b61..918d504a46e 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -5,6 +5,400 @@ #include "messages/Image.hpp" #include "util/QStringHash.hpp" +namespace { + +using namespace chatterino; + +Url getEmoteLink(const EmoteId &id, const QString &emoteScale) +{ + return {QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", id.string) + .replace("{scale}", emoteScale)}; +} + +QSize getEmoteExpectedBaseSize(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr QSize defaultBaseSize(28, 28); + static std::unordered_map outliers{ + {"555555635", {21, 18}}, /* ;p */ + {"555555636", {21, 18}}, /* ;-p */ + {"555555614", {21, 18}}, /* O_o */ + {"555555641", {21, 18}}, /* :z */ + {"555555604", {21, 18}}, /* :\\ */ + {"444", {21, 18}}, /* :| */ + {"555555634", {21, 18}}, /* ;-P */ + {"439", {21, 18}}, /* ;) */ + {"555555642", {21, 18}}, /* :-z */ + {"555555613", {21, 18}}, /* :-o */ + {"555555625", {21, 18}}, /* :-p */ + {"433", {21, 18}}, /* :/ */ + {"555555622", {21, 18}}, /* :P */ + {"555555640", {21, 18}}, /* :-| */ + {"555555623", {21, 18}}, /* :-P */ + {"555555628", {21, 18}}, /* :) */ + {"555555632", {21, 18}}, /* 8-) */ + {"555555667", {20, 18}}, /* ;p */ + {"445", {21, 18}}, /* <3 */ + {"555555668", {20, 18}}, /* ;-p */ + {"555555679", {20, 18}}, /* :z */ + {"483", {20, 18}}, /* <3 */ + {"555555666", {20, 18}}, /* ;-P */ + {"497", {20, 18}}, /* O_o */ + {"555555664", {20, 18}}, /* :-p */ + {"555555671", {20, 18}}, /* :o */ + {"555555681", {20, 18}}, /* :Z */ + {"555555672", {20, 18}}, /* :-o */ + {"555555676", {20, 18}}, /* :-\\ */ + {"555555611", {21, 18}}, /* :-O */ + {"555555670", {20, 18}}, /* :-O */ + {"555555688", {20, 18}}, /* :-D */ + {"441", {21, 18}}, /* B) */ + {"555555601", {21, 18}}, /* >( */ + {"491", {20, 18}}, /* ;P */ + {"496", {20, 18}}, /* :D */ + {"492", {20, 18}}, /* :O */ + {"555555573", {24, 18}}, /* o_O */ + {"555555643", {21, 18}}, /* :Z */ + {"1898", {26, 28}}, /* ThunBeast */ + {"555555682", {20, 18}}, /* :-Z */ + {"1896", {20, 30}}, /* WholeWheat */ + {"1906", {24, 30}}, /* SoBayed */ + {"555555607", {21, 18}}, /* :-( */ + {"555555660", {20, 18}}, /* :-( */ + {"489", {20, 18}}, /* :( */ + {"495", {20, 18}}, /* :s */ + {"555555638", {21, 18}}, /* :-D */ + {"357", {28, 30}}, /* HotPokket */ + {"555555624", {21, 18}}, /* :p */ + {"73", {21, 30}}, /* DBstyle */ + {"555555674", {20, 18}}, /* :-/ */ + {"555555629", {21, 18}}, /* :-) */ + {"555555600", {24, 18}}, /* R-) */ + {"41", {19, 27}}, /* Kreygasm */ + {"555555612", {21, 18}}, /* :o */ + {"488", {29, 24}}, /* :7 */ + {"69", {41, 28}}, /* BloodTrail */ + {"555555608", {21, 18}}, /* R) */ + {"501", {20, 18}}, /* ;) */ + {"50", {18, 27}}, /* ArsonNoSexy */ + {"443", {21, 18}}, /* :D */ + {"1904", {24, 30}}, /* BigBrother */ + {"555555595", {24, 18}}, /* ;P */ + {"555555663", {20, 18}}, /* :p */ + {"555555576", {24, 18}}, /* o.o */ + {"360", {22, 30}}, /* FailFish */ + {"500", {20, 18}}, /* B) */ + {"3", {24, 18}}, /* :D */ + {"484", {20, 22}}, /* R) */ + {"555555678", {20, 18}}, /* :-| */ + {"7", {24, 18}}, /* B) */ + {"52", {32, 32}}, /* SMOrc */ + {"555555644", {21, 18}}, /* :-Z */ + {"18", {20, 27}}, /* TheRinger */ + {"49106", {27, 28}}, /* CorgiDerp */ + {"6", {24, 18}}, /* O_o */ + {"10", {24, 18}}, /* :/ */ + {"47", {24, 24}}, /* PunchTrees */ + {"555555561", {24, 18}}, /* :-D */ + {"555555564", {24, 18}}, /* :-| */ + {"13", {24, 18}}, /* ;P */ + {"555555593", {24, 18}}, /* :p */ + {"555555589", {24, 18}}, /* ;) */ + {"555555590", {24, 18}}, /* ;-) */ + {"486", {27, 42}}, /* :> */ + {"40", {21, 27}}, /* KevinTurtle */ + {"555555558", {24, 18}}, /* :( */ + {"555555597", {24, 18}}, /* ;p */ + {"555555580", {24, 18}}, /* :O */ + {"555555567", {24, 18}}, /* :Z */ + {"1", {24, 18}}, /* :) */ + {"11", {24, 18}}, /* ;) */ + {"33", {25, 32}}, /* DansGame */ + {"555555586", {24, 18}}, /* :-/ */ + {"4", {24, 18}}, /* >( */ + {"555555588", {24, 18}}, /* :-\\ */ + {"12", {24, 18}}, /* :P */ + {"555555563", {24, 18}}, /* :| */ + {"555555581", {24, 18}}, /* :-O */ + {"555555598", {24, 18}}, /* ;-p */ + {"555555596", {24, 18}}, /* ;-P */ + {"555555557", {24, 18}}, /* :-) */ + {"498", {20, 18}}, /* >( */ + {"555555680", {20, 18}}, /* :-z */ + {"555555587", {24, 18}}, /* :\\ */ + {"5", {24, 18}}, /* :| */ + {"354", {20, 30}}, /* 4Head */ + {"555555562", {24, 18}}, /* >( */ + {"555555594", {24, 18}}, /* :-p */ + {"490", {20, 18}}, /* :P */ + {"555555662", {20, 18}}, /* :-P */ + {"2", {24, 18}}, /* :( */ + {"1902", {27, 29}}, /* Keepo */ + {"555555627", {21, 18}}, /* ;-) */ + {"555555566", {24, 18}}, /* :-z */ + {"555555559", {24, 18}}, /* :-( */ + {"555555592", {24, 18}}, /* :-P */ + {"28", {39, 27}}, /* MrDestructoid */ + {"8", {24, 18}}, /* :O */ + {"244", {24, 30}}, /* FUNgineer */ + {"555555591", {24, 18}}, /* :P */ + {"555555585", {24, 18}}, /* :/ */ + {"494", {20, 18}}, /* :| */ + {"9", {24, 18}}, /* <3 */ + {"555555584", {24, 18}}, /* <3 */ + {"555555579", {24, 18}}, /* 8-) */ + {"14", {24, 18}}, /* R) */ + {"485", {27, 18}}, /* #/ */ + {"555555560", {24, 18}}, /* :D */ + {"86", {36, 30}}, /* BibleThump */ + {"555555578", {24, 18}}, /* B-) */ + {"17", {20, 27}}, /* StoneLightning */ + {"436", {21, 18}}, /* :O */ + {"555555675", {20, 18}}, /* :\\ */ + {"22", {19, 27}}, /* RedCoat */ + {"555555574", {24, 18}}, /* o.O */ + {"555555603", {21, 18}}, /* :-/ */ + {"1901", {24, 28}}, /* Kippa */ + {"15", {21, 27}}, /* JKanStyle */ + {"555555605", {21, 18}}, /* :-\\ */ + {"555555701", {20, 18}}, /* ;-) */ + {"487", {20, 42}}, /* <] */ + {"555555572", {24, 18}}, /* O.O */ + {"65", {40, 30}}, /* FrankerZ */ + {"25", {25, 28}}, /* Kappa */ + {"36", {36, 30}}, /* PJSalt */ + {"499", {20, 18}}, /* :) */ + {"555555565", {24, 18}}, /* :z */ + {"434", {21, 18}}, /* :( */ + {"555555577", {24, 18}}, /* B) */ + {"34", {21, 28}}, /* SwiftRage */ + {"555555575", {24, 18}}, /* o_o */ + {"92", {23, 30}}, /* PMSTwin */ + {"555555570", {24, 18}}, /* O.o */ + {"555555569", {24, 18}}, /* O_o */ + {"493", {20, 18}}, /* :/ */ + {"26", {20, 27}}, /* JonCarnage */ + {"66", {20, 27}}, /* OneHand */ + {"555555568", {24, 18}}, /* :-Z */ + {"555555599", {24, 18}}, /* R) */ + {"1900", {33, 30}}, /* RalpherZ */ + {"555555582", {24, 18}}, /* :o */ + {"1899", {22, 30}}, /* TF2John */ + {"555555633", {21, 18}}, /* ;P */ + {"16", {22, 27}}, /* OptimizePrime */ + {"30", {29, 27}}, /* BCWarrior */ + {"555555583", {24, 18}}, /* :-o */ + {"32", {21, 27}}, /* GingerPower */ + {"87", {24, 30}}, /* ShazBotstix */ + {"74", {24, 30}}, /* AsianGlow */ + {"555555571", {24, 18}}, /* O_O */ + {"46", {24, 24}}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return defaultBaseSize; +} + +qreal getEmote3xScaleFactor(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr qreal default3xScaleFactor = 0.25; + static std::unordered_map outliers{ + {"555555635", 0.3333333333333333}, /* ;p */ + {"555555636", 0.3333333333333333}, /* ;-p */ + {"555555614", 0.3333333333333333}, /* O_o */ + {"555555641", 0.3333333333333333}, /* :z */ + {"555555604", 0.3333333333333333}, /* :\\ */ + {"444", 0.3333333333333333}, /* :| */ + {"555555634", 0.3333333333333333}, /* ;-P */ + {"439", 0.3333333333333333}, /* ;) */ + {"555555642", 0.3333333333333333}, /* :-z */ + {"555555613", 0.3333333333333333}, /* :-o */ + {"555555625", 0.3333333333333333}, /* :-p */ + {"433", 0.3333333333333333}, /* :/ */ + {"555555622", 0.3333333333333333}, /* :P */ + {"555555640", 0.3333333333333333}, /* :-| */ + {"555555623", 0.3333333333333333}, /* :-P */ + {"555555628", 0.3333333333333333}, /* :) */ + {"555555632", 0.3333333333333333}, /* 8-) */ + {"555555667", 0.3333333333333333}, /* ;p */ + {"445", 0.3333333333333333}, /* <3 */ + {"555555668", 0.3333333333333333}, /* ;-p */ + {"555555679", 0.3333333333333333}, /* :z */ + {"483", 0.3333333333333333}, /* <3 */ + {"555555666", 0.3333333333333333}, /* ;-P */ + {"497", 0.3333333333333333}, /* O_o */ + {"555555664", 0.3333333333333333}, /* :-p */ + {"555555671", 0.3333333333333333}, /* :o */ + {"555555681", 0.3333333333333333}, /* :Z */ + {"555555672", 0.3333333333333333}, /* :-o */ + {"555555676", 0.3333333333333333}, /* :-\\ */ + {"555555611", 0.3333333333333333}, /* :-O */ + {"555555670", 0.3333333333333333}, /* :-O */ + {"555555688", 0.3333333333333333}, /* :-D */ + {"441", 0.3333333333333333}, /* B) */ + {"555555601", 0.3333333333333333}, /* >( */ + {"491", 0.3333333333333333}, /* ;P */ + {"496", 0.3333333333333333}, /* :D */ + {"492", 0.3333333333333333}, /* :O */ + {"555555573", 0.3333333333333333}, /* o_O */ + {"555555643", 0.3333333333333333}, /* :Z */ + {"1898", 0.3333333333333333}, /* ThunBeast */ + {"555555682", 0.3333333333333333}, /* :-Z */ + {"1896", 0.3333333333333333}, /* WholeWheat */ + {"1906", 0.3333333333333333}, /* SoBayed */ + {"555555607", 0.3333333333333333}, /* :-( */ + {"555555660", 0.3333333333333333}, /* :-( */ + {"489", 0.3333333333333333}, /* :( */ + {"495", 0.3333333333333333}, /* :s */ + {"555555638", 0.3333333333333333}, /* :-D */ + {"357", 0.3333333333333333}, /* HotPokket */ + {"555555624", 0.3333333333333333}, /* :p */ + {"73", 0.3333333333333333}, /* DBstyle */ + {"555555674", 0.3333333333333333}, /* :-/ */ + {"555555629", 0.3333333333333333}, /* :-) */ + {"555555600", 0.3333333333333333}, /* R-) */ + {"41", 0.3333333333333333}, /* Kreygasm */ + {"555555612", 0.3333333333333333}, /* :o */ + {"488", 0.3333333333333333}, /* :7 */ + {"69", 0.3333333333333333}, /* BloodTrail */ + {"555555608", 0.3333333333333333}, /* R) */ + {"501", 0.3333333333333333}, /* ;) */ + {"50", 0.3333333333333333}, /* ArsonNoSexy */ + {"443", 0.3333333333333333}, /* :D */ + {"1904", 0.3333333333333333}, /* BigBrother */ + {"555555595", 0.3333333333333333}, /* ;P */ + {"555555663", 0.3333333333333333}, /* :p */ + {"555555576", 0.3333333333333333}, /* o.o */ + {"360", 0.3333333333333333}, /* FailFish */ + {"500", 0.3333333333333333}, /* B) */ + {"3", 0.3333333333333333}, /* :D */ + {"484", 0.3333333333333333}, /* R) */ + {"555555678", 0.3333333333333333}, /* :-| */ + {"7", 0.3333333333333333}, /* B) */ + {"52", 0.3333333333333333}, /* SMOrc */ + {"555555644", 0.3333333333333333}, /* :-Z */ + {"18", 0.3333333333333333}, /* TheRinger */ + {"49106", 0.3333333333333333}, /* CorgiDerp */ + {"6", 0.3333333333333333}, /* O_o */ + {"10", 0.3333333333333333}, /* :/ */ + {"47", 0.3333333333333333}, /* PunchTrees */ + {"555555561", 0.3333333333333333}, /* :-D */ + {"555555564", 0.3333333333333333}, /* :-| */ + {"13", 0.3333333333333333}, /* ;P */ + {"555555593", 0.3333333333333333}, /* :p */ + {"555555589", 0.3333333333333333}, /* ;) */ + {"555555590", 0.3333333333333333}, /* ;-) */ + {"486", 0.3333333333333333}, /* :> */ + {"40", 0.3333333333333333}, /* KevinTurtle */ + {"555555558", 0.3333333333333333}, /* :( */ + {"555555597", 0.3333333333333333}, /* ;p */ + {"555555580", 0.3333333333333333}, /* :O */ + {"555555567", 0.3333333333333333}, /* :Z */ + {"1", 0.3333333333333333}, /* :) */ + {"11", 0.3333333333333333}, /* ;) */ + {"33", 0.3333333333333333}, /* DansGame */ + {"555555586", 0.3333333333333333}, /* :-/ */ + {"4", 0.3333333333333333}, /* >( */ + {"555555588", 0.3333333333333333}, /* :-\\ */ + {"12", 0.3333333333333333}, /* :P */ + {"555555563", 0.3333333333333333}, /* :| */ + {"555555581", 0.3333333333333333}, /* :-O */ + {"555555598", 0.3333333333333333}, /* ;-p */ + {"555555596", 0.3333333333333333}, /* ;-P */ + {"555555557", 0.3333333333333333}, /* :-) */ + {"498", 0.3333333333333333}, /* >( */ + {"555555680", 0.3333333333333333}, /* :-z */ + {"555555587", 0.3333333333333333}, /* :\\ */ + {"5", 0.3333333333333333}, /* :| */ + {"354", 0.3333333333333333}, /* 4Head */ + {"555555562", 0.3333333333333333}, /* >( */ + {"555555594", 0.3333333333333333}, /* :-p */ + {"490", 0.3333333333333333}, /* :P */ + {"555555662", 0.3333333333333333}, /* :-P */ + {"2", 0.3333333333333333}, /* :( */ + {"1902", 0.3333333333333333}, /* Keepo */ + {"555555627", 0.3333333333333333}, /* ;-) */ + {"555555566", 0.3333333333333333}, /* :-z */ + {"555555559", 0.3333333333333333}, /* :-( */ + {"555555592", 0.3333333333333333}, /* :-P */ + {"28", 0.3333333333333333}, /* MrDestructoid */ + {"8", 0.3333333333333333}, /* :O */ + {"244", 0.3333333333333333}, /* FUNgineer */ + {"555555591", 0.3333333333333333}, /* :P */ + {"555555585", 0.3333333333333333}, /* :/ */ + {"494", 0.3333333333333333}, /* :| */ + {"9", 0.21428571428571427}, /* <3 */ + {"555555584", 0.21428571428571427}, /* <3 */ + {"555555579", 0.3333333333333333}, /* 8-) */ + {"14", 0.3333333333333333}, /* R) */ + {"485", 0.3333333333333333}, /* #/ */ + {"555555560", 0.3333333333333333}, /* :D */ + {"86", 0.3333333333333333}, /* BibleThump */ + {"555555578", 0.3333333333333333}, /* B-) */ + {"17", 0.3333333333333333}, /* StoneLightning */ + {"436", 0.3333333333333333}, /* :O */ + {"555555675", 0.3333333333333333}, /* :\\ */ + {"22", 0.3333333333333333}, /* RedCoat */ + {"245", 0.3333333333333333}, /* ResidentSleeper */ + {"555555574", 0.3333333333333333}, /* o.O */ + {"555555603", 0.3333333333333333}, /* :-/ */ + {"1901", 0.3333333333333333}, /* Kippa */ + {"15", 0.3333333333333333}, /* JKanStyle */ + {"555555605", 0.3333333333333333}, /* :-\\ */ + {"555555701", 0.3333333333333333}, /* ;-) */ + {"487", 0.3333333333333333}, /* <] */ + {"22639", 0.3333333333333333}, /* BabyRage */ + {"555555572", 0.3333333333333333}, /* O.O */ + {"65", 0.3333333333333333}, /* FrankerZ */ + {"25", 0.3333333333333333}, /* Kappa */ + {"36", 0.3333333333333333}, /* PJSalt */ + {"499", 0.3333333333333333}, /* :) */ + {"555555565", 0.3333333333333333}, /* :z */ + {"434", 0.3333333333333333}, /* :( */ + {"555555577", 0.3333333333333333}, /* B) */ + {"34", 0.3333333333333333}, /* SwiftRage */ + {"555555575", 0.3333333333333333}, /* o_o */ + {"92", 0.3333333333333333}, /* PMSTwin */ + {"555555570", 0.3333333333333333}, /* O.o */ + {"555555569", 0.3333333333333333}, /* O_o */ + {"493", 0.3333333333333333}, /* :/ */ + {"26", 0.3333333333333333}, /* JonCarnage */ + {"66", 0.3333333333333333}, /* OneHand */ + {"973", 0.3333333333333333}, /* DAESuppy */ + {"555555568", 0.3333333333333333}, /* :-Z */ + {"555555599", 0.3333333333333333}, /* R) */ + {"1900", 0.3333333333333333}, /* RalpherZ */ + {"555555582", 0.3333333333333333}, /* :o */ + {"1899", 0.3333333333333333}, /* TF2John */ + {"555555633", 0.3333333333333333}, /* ;P */ + {"16", 0.3333333333333333}, /* OptimizePrime */ + {"30", 0.3333333333333333}, /* BCWarrior */ + {"555555583", 0.3333333333333333}, /* :-o */ + {"32", 0.3333333333333333}, /* GingerPower */ + {"87", 0.3333333333333333}, /* ShazBotstix */ + {"74", 0.3333333333333333}, /* AsianGlow */ + {"555555571", 0.3333333333333333}, /* O_O */ + {"46", 0.3333333333333333}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return default3xScaleFactor; +} + +} // namespace + namespace chatterino { QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode) @@ -44,14 +438,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { - // From Twitch docs - expected size for an emote (1x) - constexpr QSize baseSize(28, 28); + auto baseSize = getEmoteExpectedBaseSize(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), - Image::fromUrl(getEmoteLink(id, "3.0"), 0.25, baseSize * 4), + Image::fromUrl(getEmoteLink(id, "3.0"), + getEmote3xScaleFactor(id), baseSize * 4), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); @@ -60,11 +454,4 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, return shared; } -Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) -{ - return {QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", id.string) - .replace("{scale}", emoteScale)}; -} - } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index d793ce72338..17e50b11fcb 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -52,7 +52,6 @@ class TwitchEmotes : public ITwitchEmotes const EmoteName &name) override; private: - Url getEmoteLink(const EmoteId &id, const QString &emoteScale); UniqueAccess>> twitchEmotesCache_; }; From b35f10fa540562e8c524ec31ae51e3d4fb139214 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 14:50:58 +0100 Subject: [PATCH 010/105] chore: require newline at EOF (#5278) --- .clang-format | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.clang-format b/.clang-format index 0feaad9dc10..cfbe49d31fe 100644 --- a/.clang-format +++ b/.clang-format @@ -50,3 +50,4 @@ PointerBindsToType: false SpacesBeforeTrailingComments: 2 Standard: Auto ReflowComments: false +InsertNewlineAtEOF: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b0c05006d..d89ebd1eec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,6 +198,7 @@ - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) - Dev: `clang-tidy` CI now uses Qt 6. (#5273) +- Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) ## 2.4.6 From d4b8feac7d86f941c09c4a1f54743d2882d9a54a Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 15:23:02 +0100 Subject: [PATCH 011/105] lua: Change CompletionRequested handler to use an event table. (#5280) --- CHANGELOG.md | 1 + docs/chatterino.d.ts | 14 ++++++----- docs/wip-plugins.md | 6 ++--- src/controllers/plugins/LuaAPI.hpp | 26 +++++++++++++++++++- src/controllers/plugins/LuaUtilities.cpp | 14 +++++++++++ src/controllers/plugins/LuaUtilities.hpp | 2 ++ src/controllers/plugins/Plugin.hpp | 6 ++--- src/controllers/plugins/PluginController.cpp | 8 ++++-- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89ebd1eec1..f9ec7507035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) +- Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 9bf6f57c0fb..95d2282be9f 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -75,6 +75,13 @@ declare module c2 { handler: (ctx: CommandContext) => void ): boolean; + class CompletionEvent { + query: string; + full_text_content: string; + cursor_position: number; + is_first_word: boolean; + } + class CompletionList { values: String[]; hide_others: boolean; @@ -84,12 +91,7 @@ declare module c2 { CompletionRequested = "CompletionRequested", } - type CbFuncCompletionsRequested = ( - query: string, - full_text_content: string, - cursor_position: number, - is_first_word: boolean - ) => CompletionList; + type CbFuncCompletionsRequested = (ev: CompletionEvent) => CompletionList; type CbFunc = T extends EventType.CompletionRequested ? CbFuncCompletionsRequested : never; diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 1309d7bab0c..32eda387fd1 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -167,7 +167,7 @@ Limitations/known issues: #### `register_callback("CompletionRequested", handler)` -Registers a callback (`handler`) to process completions. The callback gets the following parameters: +Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries: - `query`: The queried word. - `full_text_content`: The whole input. @@ -190,8 +190,8 @@ end c2.register_callback( "CompletionRequested", - function(query, full_text_content, cursor_position, is_first_word) - if ("!join"):startswith(query) then + function(event) + if ("!join"):startswith(event.query) then ---@type CompletionList return { hide_others = true, values = { "!join" } } end diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index df042b24f5c..39df152169a 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -5,6 +5,8 @@ extern "C" { # include } +# include "controllers/plugins/LuaUtilities.hpp" + # include # include @@ -55,6 +57,28 @@ struct CompletionList { bool hideOthers{}; }; +/** + * @lua@class CompletionEvent + */ +struct CompletionEvent { + /** + * @lua@field query string The word being completed + */ + QString query; + /** + * @lua@field full_text_content string Content of the text input + */ + QString full_text_content; + /** + * @lua@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) + */ + int cursor_position{}; + /** + * @lua@field is_first_word boolean True if this is the first word in the input + */ + bool is_first_word{}; +}; + /** * @includefile common/Channel.hpp * @includefile controllers/plugins/api/ChannelRef.hpp @@ -74,7 +98,7 @@ int c2_register_command(lua_State *L); * Registers a callback to be invoked when completions for a term are requested. * * @lua@param type "CompletionRequested" - * @lua@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. + * @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. * @exposed c2.register_callback */ int c2_register_callback(lua_State *L); diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index 9361cd1ff3e..64af18c0133 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -142,6 +142,20 @@ StackIdx push(lua_State *L, const int &b) return lua_gettop(L); } +StackIdx push(lua_State *L, const api::CompletionEvent &ev) +{ + auto idx = pushEmptyTable(L, 4); +# define PUSH(field) \ + lua::push(L, ev.field); \ + lua_setfield(L, idx, #field) + PUSH(query); + PUSH(full_text_content); + PUSH(cursor_position); + PUSH(is_first_word); +# undef PUSH + return idx; +} + bool peek(lua_State *L, int *out, StackIdx idx) { StackGuard guard(L); diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 4c78d6edc9a..5443a751f79 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -28,6 +28,7 @@ namespace chatterino::lua { namespace api { struct CompletionList; + struct CompletionEvent; } // namespace api constexpr int ERROR_BAD_PEEK = LUA_OK - 1; @@ -66,6 +67,7 @@ StackIdx push(lua_State *L, const QString &str); StackIdx push(lua_State *L, const std::string &str); StackIdx push(lua_State *L, const bool &b); StackIdx push(lua_State *L, const int &b); +StackIdx push(lua_State *L, const api::CompletionEvent &ev); // returns OK? bool peek(lua_State *L, int *out, StackIdx idx = -1); diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 4450b2a0192..2adbe9067fb 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -98,8 +98,8 @@ class Plugin // Note: The CallbackFunction object's destructor will remove the function from the lua stack using LuaCompletionCallback = - lua::CallbackFunction; + lua::CallbackFunction; std::optional getCompletionCallback() { if (this->state_ == nullptr || !this->error_.isNull()) @@ -123,7 +123,7 @@ class Plugin // move return std::make_optional>( + lua::api::CompletionList, lua::api::CompletionEvent>>( this->state_, lua_gettop(this->state_)); } diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 0f23df3430d..8c2d8055619 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -433,8 +433,12 @@ std::pair PluginController::updateCustomCompletions( qCDebug(chatterinoLua) << "Processing custom completions from plugin" << name; auto &cb = *opt; - auto errOrList = - cb(query, fullTextContent, cursorPosition, isFirstWord); + auto errOrList = cb(lua::api::CompletionEvent{ + .query = query, + .full_text_content = fullTextContent, + .cursor_position = cursorPosition, + .is_first_word = isFirstWord, + }); if (std::holds_alternative(errOrList)) { guard.handled(); From c1bd5d11d057ae15e8866511bdc9645466e5ec68 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 22:11:52 +0100 Subject: [PATCH 012/105] refactor: improve LuaLS generator (#5283) --- CHANGELOG.md | 2 +- docs/plugin-meta.lua | 71 ++-- scripts/make_luals_meta.py | 385 ++++++++++++++------- src/controllers/plugins/LuaAPI.hpp | 2 +- src/controllers/plugins/api/ChannelRef.hpp | 11 +- 5 files changed, 307 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ec7507035..01c49ca3401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -179,7 +179,7 @@ - Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747) - Dev: Refactor Args to be less of a singleton. (#5041) - Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045) -- Dev: Autogenerate docs/plugin-meta.lua. (#5055) +- Dev: Autogenerate docs/plugin-meta.lua. (#5055, #5283) - Dev: Changed Ubuntu & AppImage builders to statically link Qt. (#5151) - Dev: Refactor `NetworkPrivate`. (#5063) - Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 2cc56af5994..7b72b46d5f3 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -6,22 +6,14 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - - ----@alias LogLevel integer ----@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel } +---@alias c2.LogLevel integer +---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } c2.LogLevel = {} ----@alias EventType integer ----@type { CompletionRequested: EventType } +---@alias c2.EventType integer +---@type { CompletionRequested: c2.EventType } c2.EventType = {} + ---@class CommandContext ---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. ---@field channel Channel The channel the command was executed in. @@ -29,20 +21,31 @@ c2.EventType = {} ---@class CompletionList ---@field values string[] The completions ---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. --- Now including data from src/common/Channel.hpp. + +---@class CompletionEvent +---@field query string The word being completed +---@field full_text_content string Content of the text input +---@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) +---@field is_first_word boolean True if this is the first word in the input + +-- Begin src/common/Channel.hpp ---@alias ChannelType integer ----@type { None: ChannelType } +---@type { None: ChannelType, Direct: ChannelType, Twitch: ChannelType, TwitchWhispers: ChannelType, TwitchWatching: ChannelType, TwitchMentions: ChannelType, TwitchLive: ChannelType, TwitchAutomod: ChannelType, TwitchEnd: ChannelType, Irc: ChannelType, Misc: ChannelType } ChannelType = {} --- Back to src/controllers/plugins/LuaAPI.hpp. --- Now including data from src/controllers/plugins/api/ChannelRef.hpp. ---- This enum describes a platform for the purpose of searching for a channel. ---- Currently only Twitch is supported because identifying IRC channels is tricky. + +-- End src/common/Channel.hpp + +-- Begin src/controllers/plugins/api/ChannelRef.hpp ---@alias Platform integer +--- This enum describes a platform for the purpose of searching for a channel. +--- Currently only Twitch is supported because identifying IRC channels is tricky. ---@type { Twitch: Platform } Platform = {} ----@class Channel: IWeakResource + +---@class Channel +Channel = {} --- Returns true if the channel this object points to is valid. --- If the object expired, returns false @@ -82,11 +85,9 @@ function Channel:add_system_message(message) end --- Compares the channel Type. Note that enum values aren't guaranteed, just --- that they are equal to the exposed enum. --- ----@return bool +---@return boolean function Channel:is_twitch_channel() end ---- Twitch Channel specific functions - --- Returns a copy of the channel mode settings (subscriber only, r9k etc.) --- ---@return RoomModes @@ -119,15 +120,10 @@ function Channel:is_mod() end ---@return boolean function Channel:is_vip() end ---- Misc - ---@return string function Channel:__tostring() end ---- Static functions - --- Finds a channel by name. ---- --- Misc channels are marked as Twitch: --- - /whispers --- - /mentions @@ -142,19 +138,15 @@ function Channel.by_name(name, platform) end --- Finds a channel by the Twitch user ID of its owner. --- ----@param string id ID of the owner of the channel. +---@param id string ID of the owner of the channel. ---@return Channel? -function Channel.by_twitch_id(string) end +function Channel.by_twitch_id(id) end ---@class RoomModes ---@field unique_chat boolean You might know this as r9kbeta or robot9000. ---@field subscriber_only boolean ----@field emotes_only boolean Whether or not text is allowed in messages. - ---- Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes - ----@field unique_chat number? Time in minutes you need to follow to chat or nil. - +---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes +---@field follower_only number? Time in minutes you need to follow to chat or nil. ---@field slow_mode number? Time in seconds you need to wait before sending messages or nil. ---@class StreamStatus @@ -164,7 +156,8 @@ function Channel.by_twitch_id(string) end ---@field title string Stream title or last stream title ---@field game_name string ---@field game_id string --- Back to src/controllers/plugins/LuaAPI.hpp. + +-- End src/controllers/plugins/api/ChannelRef.hpp --- Registers a new command called `name` which when executed will call `handler`. --- @@ -176,12 +169,12 @@ function c2.register_command(name, handler) end --- Registers a callback to be invoked when completions for a term are requested. --- ---@param type "CompletionRequested" ----@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. +---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. function c2.register_callback(type, func) end --- Writes a message to the Chatterino log. --- ----@param level LogLevel The desired level. +---@param level c2.LogLevel The desired level. ---@param ... any Values to log. Should be convertible to a string with `tostring()`. function c2.log(level, ...) end diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index 22240da1984..58a06242806 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -12,25 +12,26 @@ - Do not have any useful info on '/**' and '*/' lines. - Class members are not allowed to have non-@command lines and commands different from @lua@field -When this scripts sees "@brief", any further lines of the comment will be ignored +Only entire comment blocks are used. One comment block can describe at most one +entity (function/class/enum). Blocks without commands are ignored. Valid commands are: 1. @exposeenum [dotted.name.in_lua.last_part] Define a table with keys of the enum. Values behind those keys aren't written on purpose. - This generates three lines: - - An type alias of [last_part] to integer, - - A type description that describes available values of the enum, - - A global table definition for the num -2. @lua[@command] +2. @exposed [c2.name] + Generates a function definition line from the last `@lua@param`s. +3. @lua[@command] Writes [@command] to the file as a comment, usually this is @class, @param, @return, ... @lua@class and @lua@field have special treatment when it comes to generation of spacing new lines -3. @exposed [c2.name] - Generates a function definition line from the last `@lua@param`s. Non-command lines of comments are written with a space after '---' """ + +from io import TextIOWrapper from pathlib import Path +import re +from typing import Optional BOILERPLATE = """ ---@meta Chatterino2 @@ -41,14 +42,6 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - """ repo_root = Path(__file__).parent.parent @@ -58,116 +51,274 @@ print("Writing to", lua_meta.relative_to(repo_root)) -def process_file(target, out): - print("Reading from", target.relative_to(repo_root)) - with target.open("r") as f: - lines = f.read().splitlines() +def strip_line(line: str): + return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() - # Are we in a doc comment? - comment: bool = False - # This is set when @brief is encountered, making the rest of the comment be - # ignored - ignore_this_comment: bool = False - - # Last `@lua@param`s seen - for @exposed generation - last_params_names: list[str] = [] - # Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier - is_class = False - - # The name of the next enum in lua world - expose_next_enum_as: str | None = None - # Name of the current enum in c++ world, used to generate internal typenames for - current_enum_name: str | None = None - for line_num, line in enumerate(lines): - line = line.strip() - loc = f'{target.relative_to(repo_root)}:{line_num}' - if line.startswith("enum class "): - line = line.removeprefix("enum class ") - temp = line.split(" ", 2) - current_enum_name = temp[0] - if not expose_next_enum_as: - print( - f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command" - ) - current_enum_name = None + +def is_comment_start(line: str): + return line.startswith("/**") + + +def is_enum_class(line: str): + return line.startswith("enum class") + + +def is_class(line: str): + return line.startswith(("class", "struct")) + + +class Reader: + lines: list[str] + line_idx: int + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self.line_idx = 0 + + def line_no(self) -> int: + """Returns the current line number (starting from 1)""" + return self.line_idx + 1 + + def has_next(self) -> bool: + """Returns true if there are lines left to read""" + return self.line_idx < len(self.lines) + + def peek_line(self) -> Optional[str]: + """Reads the line the cursor is at""" + if self.has_next(): + return self.lines[self.line_idx].strip() + return None + + def next_line(self) -> Optional[str]: + """Consumes and returns one line""" + if self.has_next(): + self.line_idx += 1 + return self.lines[self.line_idx - 1].strip() + return None + + def next_doc_comment(self) -> Optional[list[str]]: + """Reads a documentation comment (/** ... */) and advances the cursor""" + lines = [] + # find the start + while (line := self.next_line()) is not None and not is_comment_start(line): + pass + if line is None: + return None + + stripped = strip_line(line) + if stripped: + lines.append(stripped) + + if stripped.endswith("*/"): + return lines if lines else None + + while (line := self.next_line()) is not None: + if line.startswith("*/"): + break + + stripped = strip_line(line) + if not stripped: continue - current_enum_name = expose_next_enum_as.split(".", 1)[-1] - out.write("---@alias " + current_enum_name + " integer\n") - out.write("---@type { ") - # temp[1] is '{' - if len(temp) == 2: # no values on this line + + if stripped.startswith("@"): + lines.append(stripped) + continue + + if not lines: + lines.append(stripped) + else: + lines[-1] += "\n--- " + stripped + + return lines if lines else None + + def read_class_body(self) -> list[list[str]]: + """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" + items = [] + while (line := self.peek_line()) is not None: + if line.startswith("};"): + self.next_line() + break + if not is_comment_start(line): + self.next_line() + continue + doc = self.next_doc_comment() + if not doc: + break + items.append(doc) + return items + + def read_enum_variants(self) -> list[str]: + """The reader must be before an enum class definition (possibly with some comments before). It returns all variants.""" + items = [] + is_comment = False + while (line := self.peek_line()) is not None and not line.startswith("};"): + self.next_line() + if is_comment: + if line.endswith("*/"): + is_comment = False + continue + if line.startswith("/*"): + is_comment = True + continue + if line.startswith("//"): continue - line = temp[2] - - if current_enum_name is not None: - for i, tok in enumerate(line.split(" ")): - if tok == "};": - break - entry = tok.removesuffix(",") - if i != 0: - out.write(", ") - out.write(entry + ": " + current_enum_name) - out.write(" }\n" f"{expose_next_enum_as} = {{}}\n") - print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}") - current_enum_name = None - expose_next_enum_as = None + if line.endswith("};"): # oneline declaration + opener = line.find("{") + 1 + closer = line.find("}") + items = [ + line.split("=", 1)[0].strip() + for line in line[opener:closer].split(",") + ] + break + if line.startswith("enum class"): + continue + + items.append(line.rstrip(",")) + + return items + + +def finish_class(out, name): + out.write(f"{name} = {{}}\n") + + +def printmsg(path: Path, line: int, message: str): + print(f"{path.relative_to(repo_root)}:{line} {message}") + + +def panic(path: Path, line: int, message: str): + printmsg(path, line, message) + exit(1) + + +def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): + if not comments[0].startswith("@"): + out.write(f"--- {comments[0]}\n---\n") + comments = comments[1:] + params = [] + for comment in comments[:-1]: + if not comment.startswith("@lua"): + panic(path, line, f"Invalid function specification - got '{comment}'") + if comment.startswith("@lua@param"): + params.append(comment.split(" ", 2)[1]) + + out.write(f"---{comment.removeprefix('@lua')}\n") + + if not comments[-1].startswith("@exposed "): + panic(path, line, f"Invalid function exposure - got '{comments[-1]}'") + name = comments[-1].split(" ", 1)[1] + printmsg(path, line, f"function {name}") + lua_params = ", ".join(params) + out.write(f"function {name}({lua_params}) end\n\n") + + +def read_file(path: Path, out: TextIOWrapper): + print("Reading", path.relative_to(repo_root)) + with path.open("r") as f: + lines = f.read().splitlines() + + reader = Reader(lines) + while reader.has_next(): + doc_comment = reader.next_doc_comment() + if not doc_comment: + break + header_comment = None + if not doc_comment[0].startswith("@"): + if len(doc_comment) == 1: + continue + header_comment = doc_comment[0] + header = doc_comment[1:] + else: + header = doc_comment + + # include block + if header[0].startswith("@includefile "): + for comment in header: + if not comment.startswith("@includefile "): + panic( + path, + reader.line_no(), + f"Invalid include block - got line '{comment}'", + ) + filename = comment.split(" ", 1)[1] + out.write(f"-- Begin src/{filename}\n\n") + read_file(repo_root / "src" / filename, out) + out.write(f"-- End src/{filename}\n\n") continue - if line.startswith("/**"): - comment = True + # enum + if header[0].startswith("@exposeenum "): + if len(header) > 1: + panic( + path, + reader.line_no(), + f"Invalid enum exposure - one command expected, got {len(header)}", + ) + name = header[0].split(" ", 1)[1] + printmsg(path, reader.line_no(), f"enum {name}") + out.write(f"---@alias {name} integer\n") + if header_comment: + out.write(f"--- {header_comment}\n") + out.write("---@type { ") + out.write( + ", ".join( + [f"{variant}: {name}" for variant in reader.read_enum_variants()] + ) + ) + out.write(" }\n") + out.write(f"{name} = {{}}\n\n") continue - elif "*/" in line: - comment = False - ignore_this_comment = False - if not is_class: + # class + if header[0].startswith("@lua@class "): + name = header[0].split(" ", 1)[1] + classname = name.split(":")[0].strip() + printmsg(path, reader.line_no(), f"class {classname}") + + if header_comment: + out.write(f"--- {header_comment}\n") + out.write(f"---@class {name}\n") + # inline class + if len(header) > 1: + for field in header[1:]: + if not field.startswith("@lua@field "): + panic( + path, + reader.line_no(), + f"Invalid inline class exposure - all lines must be fields, got '{field}'", + ) + out.write(f"---{field.removeprefix('@lua')}\n") out.write("\n") - continue - if not comment: - continue - if ignore_this_comment: - continue - line = line.replace("*", "", 1).lstrip() - if line == "": - out.write("---\n") - elif line.startswith('@brief '): - # Doxygen comment, on a C++ only method - ignore_this_comment = True - elif line.startswith("@exposeenum "): - expose_next_enum_as = line.split(" ", 1)[1] - elif line.startswith("@exposed "): - exp = line.replace("@exposed ", "", 1) - params = ", ".join(last_params_names) - out.write(f"function {exp}({params}) end\n") - print(f"{loc} Wrote function {exp}(...)") - last_params_names = [] - elif line.startswith("@includefile "): - filename = line.replace("@includefile ", "", 1) - output.write(f"-- Now including data from src/{filename}.\n") - process_file(repo_root / 'src' / filename, output) - output.write(f'-- Back to {target.relative_to(repo_root)}.\n') - elif line.startswith("@lua"): - command = line.replace("@lua", "", 1) - if command.startswith("@param"): - last_params_names.append(command.split(" ", 2)[1]) - elif command.startswith("@class"): - print(f"{loc} Writing {command}") - if is_class: - out.write("\n") - is_class = True - elif not command.startswith("@field"): - is_class = False - - out.write("---" + command + "\n") - else: - if is_class: - is_class = False + continue + + # class definition + # save functions for later (print fields first) + funcs = [] + for comment in reader.read_class_body(): + if comment[-1].startswith("@exposed "): + funcs.append(comment) + continue + if len(comment) > 1 or not comment[0].startswith("@lua"): + continue + out.write(f"---{comment[0].removeprefix('@lua')}\n") + + if funcs: + # only define global if there are functions on the class + out.write(f"{classname} = {{}}\n\n") + else: out.write("\n") - # note the space difference from the branch above - out.write("--- " + line + "\n") + for func in funcs: + write_func(path, reader.line_no(), func, out) + continue + + # global function + if header[-1].startswith("@exposed "): + write_func(path, reader.line_no(), doc_comment, out) + continue -with lua_meta.open("w") as output: - output.write(BOILERPLATE[1:]) # skip the newline after triple quote - process_file(lua_api_file, output) +if __name__ == "__main__": + with lua_meta.open("w") as output: + output.write(BOILERPLATE[1:]) # skip the newline after triple quote + read_file(lua_api_file, output) diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 39df152169a..15be99c6fa8 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -106,7 +106,7 @@ int c2_register_callback(lua_State *L); /** * Writes a message to the Chatterino log. * - * @lua@param level LogLevel The desired level. + * @lua@param level c2.LogLevel The desired level. * @lua@param ... any Values to log. Should be convertible to a string with `tostring()`. * @exposed c2.log */ diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 29f5173d28e..abc6b421f6f 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -21,7 +21,7 @@ enum class LPlatform { }; /** - * @lua@class Channel: IWeakResource + * @lua@class Channel */ struct ChannelRef { static void createMetatable(lua_State *L); @@ -100,7 +100,7 @@ struct ChannelRef { * Compares the channel Type. Note that enum values aren't guaranteed, just * that they are equal to the exposed enum. * - * @lua@return bool + * @lua@return boolean * @exposed Channel:is_twitch_channel */ static int is_twitch_channel(lua_State *L); @@ -193,7 +193,7 @@ struct ChannelRef { /** * Finds a channel by the Twitch user ID of its owner. * - * @lua@param string id ID of the owner of the channel. + * @lua@param id string ID of the owner of the channel. * @lua@return Channel? * @exposed Channel.by_twitch_id */ @@ -216,13 +216,12 @@ struct LuaRoomModes { bool subscriber_only = false; /** - * @lua@field emotes_only boolean Whether or not text is allowed in messages. - * Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes */ bool emotes_only = false; /** - * @lua@field unique_chat number? Time in minutes you need to follow to chat or nil. + * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. */ std::optional follower_only; /** From b991b957f0cc5cd491161bf96191acc5e03ebae5 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 31 Mar 2024 11:46:58 +0200 Subject: [PATCH 013/105] fix: missing rerender on clear (#5282) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c49ca3401..af86fe2737d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) +- Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) - 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) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 2795e5ffe7b..c76373d5679 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -782,6 +782,7 @@ void ChannelView::clearMessages() this->scrollBar_->setMaximum(0); this->scrollBar_->setMinimum(0); this->queueLayout(); + this->update(); this->lastMessageHasAlternateBackground_ = false; this->lastMessageHasAlternateBackgroundReverse_ = true; From 694d53ad20d853e0d85e7c6a0a4ed57c9ee7e7ef Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 31 Mar 2024 13:07:43 +0200 Subject: [PATCH 014/105] Fix some documentations & comments (#5286) * add comments for the new reward filters * slightly improve documentation of r9k values --- src/controllers/filters/lang/Filter.cpp | 3 +++ src/widgets/settingspages/GeneralPage.cpp | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index ef0cfd15ce2..9c3ecb0228b 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -50,6 +50,9 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) * message.content * message.length * + * reward.title + * reward.cost + * reward.id */ using MessageFlag = chatterino::MessageFlag; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 74746be8ad1..d129e46eb71 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -462,7 +462,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addTitle("Messages"); layout.addCheckbox( "Separate with lines", s.separateMessages, false, - "Adds a line inbetween each message to help better tell them apart."); + "Adds a line between each message to help better tell them apart."); layout.addCheckbox("Alternate background color", s.alternateMessages, false, "Slightly change the background behind every other " "message to help better tell them apart."); @@ -904,7 +904,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) toggleLocalr9kShortcut + "."); layout.addCheckbox("Hide similar messages", s.similarityEnabled); //layout.addCheckbox("Gray out matches", s.colorSimilarDisabled); - layout.addCheckbox("By the same user", s.hideSimilarBySameUser); + layout.addCheckbox( + "By the same user", s.hideSimilarBySameUser, false, + "When checked, messages that are very similar to each other can still " + "be shown as long as they're sent by different users."); layout.addCheckbox("Hide my own messages", s.hideSimilarMyself); layout.addCheckbox("Receive notification sounds from hidden messages", s.shownSimilarTriggerHighlights); @@ -920,7 +923,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToFloat(args.value, 0.9f); - }); + }, + true, + "A value of 0.9 means the messages need to be 90% similar to be marked " + "as similar."); layout.addDropdown( "Maximum delay between messages", {"5s", "10s", "15s", "30s", "60s", "120s"}, s.hideSimilarMaxDelay, @@ -929,7 +935,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToInt(args.value, 5); - }); + }, + true, + "A value of 5s means if there's a 5s break between messages, we will " + "stop looking further through the messages for similarities."); layout.addDropdown( "Amount of previous messages to check", {"1", "2", "3", "4", "5"}, s.hideSimilarMaxMessagesToCheck, From 905aa4e923006dc989d8febbebf1da1cc512a5d5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 1 Apr 2024 00:04:11 +0200 Subject: [PATCH 015/105] refactor: About page (#5287) --- CHANGELOG.md | 1 + resources/avatars/anon.png | Bin 0 -> 1612 bytes resources/contributors.txt | 141 ++++++------- src/CMakeLists.txt | 3 + src/widgets/layout/FlowLayout.cpp | 252 ++++++++++++++++++++++++ src/widgets/layout/FlowLayout.hpp | 104 ++++++++++ src/widgets/settingspages/AboutPage.cpp | 60 ++++-- 7 files changed, 474 insertions(+), 87 deletions(-) create mode 100644 resources/avatars/anon.png create mode 100644 src/widgets/layout/FlowLayout.cpp create mode 100644 src/widgets/layout/FlowLayout.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index af86fe2737d..c41fd957019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) +- Minor: Changed the layout of the about page. (#5287) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/resources/avatars/anon.png b/resources/avatars/anon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7993edcbbbed49dc84078ac0ba2c44a3c7d2d91 GIT binary patch literal 1612 zcmV-S2DABzP)C00033P)t-s00030 z|NsC0|L5oD)YR0lu&~U`%-r1EudlDWySu2UsLRXCs;a8Mz`&ZCn#IM%kB^U_prC$! ze!#%Mn3$NNqN2sc#gmhhwY9Z(cXyeYnZLikj*gC-o1444yN{2LudlD0o14A8y>4!9 ziHV7voSch`i?+75iHV7CW*rKF^!p`oFho12xDm64H=jg5_pii(Ga zhk}BFfq{X5fPjC0e|~;`dwY9&dU|$tc5`!cZfH9JB{OjlQ2V{3ADfP;vUl9`{Us+kUJ^Yixh`Tzg_|NsC0|NsB~{wJ}e5&!@Mwn;=mRCwC$np;=eOcaLqB;kxGf=5&+ zBBCgMia`A|0XZfha{m8+r8CK7jT*oTV_aQpo~vH8ect`AD5%FPC>2_gr;{O%X9X)a*Z`9rWSGF`w*{tG{>%_eyPbN%E+)M_>I2fp(=3vez# zt2XK%(J=hKh!rKR@y=Toae*o4;u?t}T zz4|<`?Bj!=%$ka1K18_j!*>**)rZ)QnVk9&yaff%G*0_xp!CNJ7!W=2ZzK4Kw4?Ir zpW(ZISAf=VK3#eK85g`uCNce#p2^@eH zFvkAzcr+Xi2mg7%EzKQ+wEn*HGI|y)Mr-zc>FFV z11{bGIRUkA7l8eS(Y=(wEfgF8zkxCJcd;K+@Pb=Nc@m&GW%HW^!8OkD38;hd2p<6R z8%N+8DRP4&wg8Kq1a7g91JszGqA%Y8&v=_SK-~<0U!Y;5Ct(Tl8OIy(l~k=|7QpIR`+*x5s@fe4i2q%HtOAShU)LMgVc_x7r@ZWyaWE&`m`P}RZf zoB*9eX}?bXL%A-yKI41O()bn(Y&R+w>bo zG05i($w$y|pj#1I&0mK*J3EJ@ds}b%3734fkYpQhY@v;5wVEa^VyEL%3S2q}mZ{&l z1sZ|T!RR;u*90iZ0ywrnU~*f}xa5(*b7^3fX0YW{&*1YG;>JRB0#*|}v-9o%WsN21 zHpYC40a%+oV+}GgtWCJQlA7Pgz*AV+UkC#S{N?ybT0h@MP>ciG%Icr%vk?Rw3ETcHL=GbQ*+pa`GVsJp zg&Dh5DUvKMv=@)U2)LqT%~o?!2^hY;Y +#include +#include +#include + +namespace { + +using namespace chatterino; + +class Linebreak : public QWidget +{ +}; + +} // namespace + +namespace chatterino { + +FlowLayout::FlowLayout(QWidget *parent, Options options) + : QLayout(parent) + , hSpace_(options.hSpacing) + , vSpace_(options.vSpacing) +{ + if (options.margin >= 0) + { + this->setContentsMargins(options.margin, options.margin, options.margin, + options.margin); + } +} + +FlowLayout::FlowLayout(Options options) + : FlowLayout(nullptr, options) +{ +} + +FlowLayout::~FlowLayout() +{ + for (auto *item : this->itemList_) + { + delete item; + } + this->itemList_ = {}; +} + +void FlowLayout::addItem(QLayoutItem *item) +{ + this->itemList_.push_back(item); +} + +void FlowLayout::addLinebreak(int height) +{ + auto *linebreak = new Linebreak; + linebreak->setFixedHeight(height); + this->addWidget(linebreak); +} + +int FlowLayout::horizontalSpacing() const +{ + if (this->hSpace_ >= 0) + { + return this->hSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutHorizontalSpacing); +} + +void FlowLayout::setHorizontalSpacing(int value) +{ + if (this->hSpace_ == value) + { + return; + } + this->hSpace_ = value; + this->invalidate(); +} + +int FlowLayout::verticalSpacing() const +{ + if (this->vSpace_ >= 0) + { + return this->vSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutVerticalSpacing); +} + +void FlowLayout::setVerticalSpacing(int value) +{ + if (this->vSpace_ == value) + { + return; + } + this->vSpace_ = value; + this->invalidate(); +} + +int FlowLayout::count() const +{ + return static_cast(this->itemList_.size()); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + return this->itemList_[static_cast(index)]; + } + return nullptr; +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + auto *it = this->itemList_[static_cast(index)]; + this->itemList_.erase(this->itemList_.cbegin() + + static_cast(index)); + return it; + } + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return {}; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + return this->doLayout({0, 0, width, 0}, true); +} + +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + this->doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return this->minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const auto *item : this->itemList_) + { + size = size.expandedTo(item->minimumSize()); + } + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), + margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const +{ + auto margins = this->contentsMargins(); + QRect effectiveRect = rect.adjusted(margins.left(), margins.top(), + -margins.right(), -margins.bottom()); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + for (QLayoutItem *item : this->itemList_) + { + auto *linebreak = dynamic_cast(item->widget()); + if (linebreak) + { + item->setGeometry({x, y, 0, linebreak->height()}); + x = effectiveRect.x(); + y = y + lineHeight + linebreak->height(); + lineHeight = 0; + continue; + } + + auto space = this->getSpacing(item); + int nextX = x + item->sizeHint().width() + space.width(); + if (nextX - space.width() > effectiveRect.right() && lineHeight > 0) + { + x = effectiveRect.x(); + y = y + lineHeight + space.height(); + nextX = x + item->sizeHint().width() + space.width(); + lineHeight = 0; + } + + if (!testOnly) + { + item->setGeometry({QPoint{x, y}, item->sizeHint()}); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + + return y + lineHeight - rect.y() + margins.bottom(); +} + +int FlowLayout::defaultSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) + { + return -1; + } + if (auto *widget = dynamic_cast(parent)) + { + return widget->style()->pixelMetric(pm, nullptr, widget); + } + if (auto *layout = dynamic_cast(parent)) + { + return layout->spacing(); + } + return -1; +} + +QSize FlowLayout::getSpacing(QLayoutItem *item) const +{ + // called if there isn't any parent or the parent can't provide any spacing + auto fallbackSpacing = [&](auto dir) { + if (auto *widget = item->widget()) + { + return widget->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, dir); + } + if (auto *layout = item->layout()) + { + return layout->spacing(); + } + return 0; + }; + + QSize spacing(this->horizontalSpacing(), this->verticalSpacing()); + if (spacing.width() == -1) + { + spacing.rwidth() = fallbackSpacing(Qt::Horizontal); + } + if (spacing.height() == -1) + { + spacing.rheight() = fallbackSpacing(Qt::Vertical); + } + return spacing; +} + +} // namespace chatterino diff --git a/src/widgets/layout/FlowLayout.hpp b/src/widgets/layout/FlowLayout.hpp new file mode 100644 index 00000000000..39a359ff143 --- /dev/null +++ b/src/widgets/layout/FlowLayout.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +#include + +namespace chatterino { + +/// @brief A QLayout wrapping items +/// +/// Similar to a box layout that wraps its items. It's not super optimized. +/// Some computations in #doLayout() could be cached. +/// +/// This is based on the Qt flow layout example: +/// https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html +class FlowLayout : public QLayout +{ +public: + struct Options { + int margin = -1; + int hSpacing = -1; + int vSpacing = -1; + }; + + explicit FlowLayout(QWidget *parent, Options options = {-1, -1, -1}); + explicit FlowLayout(Options options = {-1, -1, -1}); + + ~FlowLayout() override; + FlowLayout(const FlowLayout &) = delete; + FlowLayout(FlowLayout &&) = delete; + FlowLayout &operator=(const FlowLayout &) = delete; + FlowLayout &operator=(FlowLayout &&) = delete; + + /// @brief Adds @a item to this layout + /// + /// Ownership of @a item is transferred. This method isn't usually called + /// in application code (use addWidget/addLayout). + /// See QLayout::addItem for more information. + void addItem(QLayoutItem *item) override; + + /// @brief Adds a linebreak to this layout + /// + /// @param height Specifies the height of the linebreak + void addLinebreak(int height = 0); + + /// @brief Spacing on the horizontal axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int horizontalSpacing() const; + + /// Setter for #horizontalSpacing(). -1 to use defaults. + void setHorizontalSpacing(int value); + + /// @brief Spacing on the vertical axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int verticalSpacing() const; + + /// Setter for #verticalSpacing(). -1 to use defaults. + void setVerticalSpacing(int value); + + /// From QLayout. This layout doesn't expand in any direction. + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; + + QSize minimumSize() const override; + QSize sizeHint() const override; + + void setGeometry(const QRect &rect) override; + + int count() const override; + QLayoutItem *itemAt(int index) const override; + + /// From QLayout. Ownership is transferred to the caller + QLayoutItem *takeAt(int index) override; + +private: + /// @brief Computes the layout + /// + /// @param rect The area in which items can be layed out + /// @param testOnly If set, items won't be moved, only the total height + /// will be computed. + /// @returns The total height including margins. + int doLayout(const QRect &rect, bool testOnly) const; + + /// @brief Computes the default spacing based for items on the parent + /// + /// @param pm Either PM_LayoutHorizontalSpacing or PM_LayoutVerticalSpacing + /// for the respective direction. + /// @returns The spacing in dp, -1 if there isn't any parent + int defaultSpacing(QStyle::PixelMetric pm) const; + + /// Computes the spacing for @a item + QSize getSpacing(QLayoutItem *item) const; + + std::vector itemList_; + int hSpace_ = -1; + int vSpace_ = -1; + int lineSpacing_ = -1; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 78597c5fca3..89c985c5e8d 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -8,6 +8,7 @@ #include "util/RemoveScrollAreaBackground.hpp" #include "widgets/BasePopup.hpp" #include "widgets/helper/SignalLabel.hpp" +#include "widgets/layout/FlowLayout.hpp" #include #include @@ -54,6 +55,7 @@ AboutPage::AboutPage() auto label = vbox.emplace(version.buildString() + "
" + version.runningString()); + label->setWordWrap(true); label->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextBrowserInteraction); } @@ -137,15 +139,15 @@ AboutPage::AboutPage() l.emplace("Facebook emojis provided by Facebook")->setOpenExternalLinks(true); l.emplace("Apple emojis provided by Apple")->setOpenExternalLinks(true); l.emplace("Google emojis provided by Google")->setOpenExternalLinks(true); - l.emplace("Emoji datasource provided by Cal Henderson" + l.emplace("Emoji datasource provided by Cal Henderson " "(show license)")->setOpenExternalLinks(true); // clang-format on } // Contributors - auto contributors = layout.emplace("Contributors"); + auto contributors = layout.emplace("People"); { - auto l = contributors.emplace(); + auto l = contributors.emplace(); QFile contributorsFile(":/contributors.txt"); contributorsFile.open(QFile::ReadOnly); @@ -166,11 +168,24 @@ AboutPage::AboutPage() continue; } + if (line.startsWith(u"@header")) + { + if (l->count() != 0) + { + l->addLinebreak(20); + } + auto *label = new QLabel(QStringLiteral("

%1

") + .arg(line.mid(8).trimmed())); + l->addWidget(label); + l->addLinebreak(8); + continue; + } + QStringList contributorParts = line.split("|"); - if (contributorParts.size() != 4) + if (contributorParts.size() != 3) { - qCDebug(chatterinoWidget) + qCWarning(chatterinoWidget) << "Missing parts in line" << line; continue; } @@ -178,39 +193,42 @@ AboutPage::AboutPage() QString username = contributorParts[0].trimmed(); QString url = contributorParts[1].trimmed(); QString avatarUrl = contributorParts[2].trimmed(); - QString role = contributorParts[3].trimmed(); auto *usernameLabel = new QLabel("" + username + ""); usernameLabel->setOpenExternalLinks(true); - auto *roleLabel = new QLabel(role); + usernameLabel->setToolTip(url); - auto contributorBox2 = l.emplace(); + auto contributorBox2 = l.emplace(); - const auto addAvatar = [&avatarUrl, &contributorBox2] { - if (!avatarUrl.isEmpty()) + const auto addAvatar = [&] { + auto *avatar = new QLabel(); + QPixmap avatarPixmap; + if (avatarUrl.isEmpty()) + { + // TODO: or anon.png + avatarPixmap.load(":/avatars/anon.png"); + } + else { - QPixmap avatarPixmap; avatarPixmap.load(avatarUrl); - - auto avatar = contributorBox2.emplace(); - avatar->setPixmap(avatarPixmap); - avatar->setFixedSize(64, 64); - avatar->setScaledContents(true); } + + avatar->setPixmap(avatarPixmap); + avatar->setFixedSize(64, 64); + avatar->setScaledContents(true); + contributorBox2->addWidget(avatar, 0, Qt::AlignCenter); }; - const auto addLabels = [&contributorBox2, &usernameLabel, - &roleLabel] { + const auto addLabels = [&] { auto *labelBox = new QVBoxLayout(); contributorBox2->addLayout(labelBox); - labelBox->addWidget(usernameLabel); - labelBox->addWidget(roleLabel); + labelBox->addWidget(usernameLabel, 0, Qt::AlignCenter); }; - addLabels(); addAvatar(); + addLabels(); } } } From 2a447d3c950cf683414d4fa716846de499e1e68c Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 1 Apr 2024 20:51:12 +0200 Subject: [PATCH 016/105] fix: use 3x scale factor for base size multiplier (#5291) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchEmotes.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c41fd957019..29850b5c3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,7 +106,7 @@ - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) -- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279, #5291) - Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 918d504a46e..4baa13f2027 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -439,13 +439,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { auto baseSize = getEmoteExpectedBaseSize(id); + auto emote3xScaleFactor = getEmote3xScaleFactor(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), - Image::fromUrl(getEmoteLink(id, "3.0"), - getEmote3xScaleFactor(id), baseSize * 4), + Image::fromUrl(getEmoteLink(id, "3.0"), emote3xScaleFactor, + baseSize * (1.0 / emote3xScaleFactor)), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); From 92e75784fce0fe5ae0ce892cfb96863432875a1a Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 2 Apr 2024 03:50:53 -0700 Subject: [PATCH 017/105] feat: report duration for multi-month anon sub gifts (#5293) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 42 ++++++++++++++++++++++ src/util/SampleData.cpp | 6 ++++ 3 files changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29850b5c3c3..4a1431df1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Minor: Changed the layout of the about page. (#5287) +- Minor: Add duration to multi-month anon sub gift messages. (#5293) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 8a9cc9e6c2c..afda22592f2 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -53,6 +53,8 @@ const QSet SPECIAL_MESSAGE_TYPES{ "viewermilestone", // watch streak, but other categories possible in future }; +const QString ANONYMOUS_GIFTER_ID = "274598607"; + MessagePtr generateBannedMessage(bool confirmedBan) { const auto linkColor = MessageColor(MessageColor::Link); @@ -516,6 +518,26 @@ std::vector parseUserNoticeMessage(Channel *channel, { messageText = "Announcement"; } + else if (msgType == "subgift" && + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + messageText = + QString("An anonymous user gifted %1 months of a Tier " + "%2 sub to %3!") + .arg(QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); @@ -1010,6 +1032,26 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } + else if (msgType == "subgift" && + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + messageText = + QString("An anonymous user gifted %1 months of a Tier " + "%2 sub to %3!") + .arg(QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 953646139bd..2c5b7ca4f8e 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -70,6 +70,12 @@ const QStringList &getSampleSubMessages() // hyperbolicxd gifted a sub to quote_if_nam R"(@badges=subscriber/0,premium/1;color=#00FF7F;display-name=hyperbolicxd;emotes=;id=b20ef4fe-cba8-41d0-a371-6327651dc9cc;login=hyperbolicxd;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=quote_if_nam;msg-param-recipient-id=217259245;msg-param-recipient-user-name=quote_if_nam;msg-param-sender-count=1;msg-param-sub-plan-name=Channel\sSubscription\s(nymn_hs);msg-param-sub-plan=1000;room-id=62300805;subscriber=1;system-msg=hyperbolicxd\sgifted\sa\sTier\s1\ssub\sto\squote_if_nam!\sThis\sis\stheir\sfirst\sGift\sSub\sin\sthe\schannel!;tmi-sent-ts=1528190938558;turbo=0;user-id=111534250;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", + // multi-month sub gift + R"(@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\sSubscription\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\sgifted\s6\smonths\sof\sTier\s1\sto\skimmi_tm.\sThey've\sgifted\s334\smonths\sin\sthe\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #mxddy)", + + // multi-month anon sub gift + R"(@msg-param-goal-user-contributions=1;system-msg=An\sanonymous\suser\sgifted\sa\sTier\s1\ssub\sto\sMohammadrezaDH!\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\slee\sgoal\s:-) :tmi.twitch.tv USERNOTICE #jmarianne)", + // first time sub R"(@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\sjust\ssubscribed\swith\sTwitch\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", From 8db0bb464da134f8032583d1043300159a40edee Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 2 Apr 2024 21:59:59 +0200 Subject: [PATCH 018/105] fix: use login name when parsing highlights (#5295) --- CHANGELOG.md | 1 + src/messages/SharedMessageBuilder.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1431df1a4..34c63cdb4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) - Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) +- Bugfix: Fixed highlights triggering for ignored users in announcements. (#5295) - 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) diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index 87a3ae9b458..98ec30473f4 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -150,7 +150,7 @@ void SharedMessageBuilder::parseUsername() void SharedMessageBuilder::parseHighlights() { - if (getSettings()->isBlacklistedUser(this->ircMessage->nick())) + if (getSettings()->isBlacklistedUser(this->message().loginName)) { // Do nothing. We ignore highlights from this user. return; @@ -158,7 +158,7 @@ void SharedMessageBuilder::parseHighlights() auto badges = SharedMessageBuilder::parseBadgeTag(this->tags); auto [highlighted, highlightResult] = getIApp()->getHighlights()->check( - this->args, badges, this->ircMessage->nick(), this->originalMessage_, + this->args, badges, this->message().loginName, this->originalMessage_, this->message().flags); if (!highlighted) From 2ea24c1a9dfdd9a2496b4636ee8757eb76a66d6a Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 3 Apr 2024 21:08:52 +0200 Subject: [PATCH 019/105] fix: use `deleteLater` for network objects and order them (#5297) --- CHANGELOG.md | 2 +- src/common/network/NetworkManager.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c63cdb4a2..88793ee3b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,7 +201,7 @@ - Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) -- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) +- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254, #5297) - Dev: `clang-tidy` CI now uses Qt 6. (#5273) - Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) diff --git a/src/common/network/NetworkManager.cpp b/src/common/network/NetworkManager.cpp index 956c2e79f31..eb1b7ec5229 100644 --- a/src/common/network/NetworkManager.cpp +++ b/src/common/network/NetworkManager.cpp @@ -24,15 +24,19 @@ void NetworkManager::deinit() assert(NetworkManager::workerThread); assert(NetworkManager::accessManager); + // delete the access manager first: + // - put the event on the worker thread + // - wait for it to process + NetworkManager::accessManager->deleteLater(); + NetworkManager::accessManager = nullptr; + if (NetworkManager::workerThread) { NetworkManager::workerThread->quit(); NetworkManager::workerThread->wait(); } - delete NetworkManager::accessManager; - NetworkManager::accessManager = nullptr; - delete NetworkManager::workerThread; + NetworkManager::workerThread->deleteLater(); NetworkManager::workerThread = nullptr; } From 25a69fd10e8ada6015d8b937f6c82ce220c742e4 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 7 Apr 2024 12:03:14 +0200 Subject: [PATCH 020/105] Release v2.5.0-beta.1 (#5303) --- .CI/chatterino-installer.iss | 2 +- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 +- docs/make-release.md | 4 +++- resources/com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss index 2e3edbf5202..fddd668f98c 100644 --- a/.CI/chatterino-installer.iss +++ b/.CI/chatterino-installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Chatterino" -#define MyAppVersion "2.4.6" +#define MyAppVersion "2.5.0" #define MyAppPublisher "Chatterino Team" #define MyAppURL "https://www.chatterino.com" #define MyAppExeName "chatterino.exe" diff --git a/CHANGELOG.md b/CHANGELOG.md index 88793ee3b11..e9ca57c90c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.5.0-beta.1 + - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) - Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) - Major: Moderators can now see restricted chat messages and suspicious treatment updates. (#5056, #5060) diff --git a/CMakeLists.txt b/CMakeLists.txt index 14efcb0daf8..3a08cf79ee1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ if(BUILD_BENCHMARKS) endif() project(chatterino - VERSION 2.4.6 + VERSION 2.5.0 DESCRIPTION "Chat client for twitch.tv" HOMEPAGE_URL "https://chatterino.com/" ) diff --git a/docs/make-release.md b/docs/make-release.md index c28dead6bdb..1509289fd39 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -8,7 +8,9 @@ - [ ] Add a new release at the top of the `releases` key in `resources/com.chatterino.chatterino.appdata.xml` This cannot use dash to denote a pre-release identifier, you have to use a tilde instead. -- [ ] Updated version code in `.CI/chatterino-installer.iss` +- [ ] Updated version code in `.CI/chatterino-installer.iss` + This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0` + - [ ] Update the changelog `## Unreleased` section to the new version `CHANGELOG.md` Make sure to leave the `## Unreleased` line unchanged for easier merges diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index ce9e25db88d..5f76c9f6d42 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1 + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.6 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 5d978b19a88..3673e5b23f1 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.4.6" +#define CHATTERINO_VERSION "2.5.0-beta.1" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From dd62707d535cd556f89ee3080ac234cf2ba5c426 Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 9 Apr 2024 14:25:08 +0200 Subject: [PATCH 021/105] fix: hide tooltip on window leave event (#5309) --- CHANGELOG.md | 2 ++ src/widgets/BaseWindow.cpp | 1 + src/widgets/BaseWindow.hpp | 1 + src/widgets/splits/SplitHeader.cpp | 14 ++++++++++++++ 4 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ca57c90c6..10c71cf5fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) + ## 2.5.0-beta.1 - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index c819c8f4a88..a61322fcec0 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -527,6 +527,7 @@ void BaseWindow::changeEvent(QEvent *) void BaseWindow::leaveEvent(QEvent *) { + this->leaving.invoke(); } void BaseWindow::moveTo(QPoint point, widgets::BoundsChecking mode) diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index e59c8ae8173..b9f21b08d3e 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -85,6 +85,7 @@ class BaseWindow : public BaseWidget void setTopMost(bool topMost); pajlada::Signals::NoArgSignal closing; + pajlada::Signals::NoArgSignal leaving; static bool supportsCustomWindowFrame(); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index b8b811ca4c4..ed4a59b185e 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -257,6 +257,20 @@ SplitHeader::SplitHeader(Split *split) getSettings()->headerStreamTitle.connect(_, this->managedConnections_); getSettings()->headerGame.connect(_, this->managedConnections_); getSettings()->headerUptime.connect(_, this->managedConnections_); + + auto *window = dynamic_cast(this->window()); + if (window) + { + // Hack: In some cases Qt doesn't send the leaveEvent the "actual" last mouse receiver. + // This can happen when quickly moving the mouse out of the window and right clicking. + // To prevent the tooltip from getting stuck, we use the window's leaveEvent. + this->managedConnections_.managedConnect(window->leaving, [this] { + if (this->tooltipWidget_->isVisible()) + { + this->tooltipWidget_->hide(); + } + }); + } } void SplitHeader::initializeLayout() From 116e82dcc508fb3b8a7e7f3c27953898b00e31fb Mon Sep 17 00:00:00 2001 From: nealxm <88364802+nealxm@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:33:58 -0700 Subject: [PATCH 022/105] fix: the version string not showing up as expected in Finder on macOS (#5311) --- CHANGELOG.md | 1 + cmake/MacOSXBundleInfo.plist.in | 2 -- src/CMakeLists.txt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c71cf5fe8..990cca03d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) +- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) ## 2.5.0-beta.1 diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index ae08eb0d9bd..9077068dbcb 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -6,8 +6,6 @@ English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa545a644da..eb64bcf31f0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -986,7 +986,6 @@ if (APPLE AND BUILD_APP) PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Chatterino" MACOSX_BUNDLE_GUI_IDENTIFIER "com.chatterino" - MACOSX_BUNDLE_INFO_STRING "Chat client for Twitch" MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" From 326f71aef8e663d307838ffe2b127c4df454088b Mon Sep 17 00:00:00 2001 From: nealxm <88364802+nealxm@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:01:46 -0700 Subject: [PATCH 023/105] add nealxm to contributors list (#5312) --- resources/avatars/nealxm.png | Bin 0 -> 2689 bytes resources/contributors.txt | 1 + 2 files changed, 1 insertion(+) create mode 100644 resources/avatars/nealxm.png diff --git a/resources/avatars/nealxm.png b/resources/avatars/nealxm.png new file mode 100644 index 0000000000000000000000000000000000000000..fcba49189842ad18ae624e60c3cbc0a784b286e8 GIT binary patch literal 2689 zcmbW%c{J2}9|!Q?FwBf4VJx{ATZ|zwMxv3VE|a)stO;c+Yh6ONYf!|**w?PD!6->I zBcv=@Qk1P?lzj|k7n5}!-TRz-onT4*(z@&|d-m9S|2c zn1>evh4Jwp1h_!l++1L89v(1wzxl`h3;+xBh{$Uh@rvSIAPW9)baZMlRPl6mix{DA zSxMV9AO^-KE+KhH>aem3QWb^KIi`DDPv7{=*E(SR zFevzDNNCut+p%$X;uA<|=^2?>**Up+_bDZ%Wz_Nq4{M&%=(W%4>Kj^LwY7J2zV3R{ zKQQ>=C{*G;@YEJNI>Eb?y852K&e676{<}o3nra4f+p{@VFqlVPi&w-54{`AqRX|5W;iprJt6N}-+Jt2>*ML4gaV5;e;gz4HzeN8XDCYkX z{R8^P!(jjd+@Sr1aSH>6z$yb-Dx+pR+eV=uL6)6JFmZ6Sx z;Q+&g;B*ck!vVhaC!wr9{-zjF~&M;U4}q*buBnrccpKtbV;B@W=6JpX{mD&hby zeSF}waLGFxnUOOp8`e!@2zJlW4afaAwelfyLrdcCf#cXHB9*d@Cfe*I`G)rKGWwC% zPx}-6O={vf0L@>f+i1AOm2jKhCOpzT7mrUNaXu1K}M!W3bwm3j6 zVmOWMzDsO<2Av^AcF6BiFV)8c&Ji1%s(uhSz*Tc{#n09?Sz5-6M58M~+o`vD^zWS$ z5_`ZR!^gdDPZjmZ<)X#N9}SY>%{i98VWI{AayTu{ZWw4u;Q-+(Afo6_g+5*Ecs+W~ zDoDAgft=(~f7q{YOy+`r^p65mzCW9Tb>re+gJY`WIefJ#IBqr30)aFp>~=svTo)-LRi z&dlj9X5C?UE%Wuf&}vVxHx!?qPBsT~Uob&jxYpy$d)L;K=^B)#6Ql>W`a0O);^c4! zciKdMGrsgj93mu$4OzjgQ9cs|yF54n>@ zdlJ9IV(Y*Ht6`wQ5BJng++znmv%f0`zLPE!88ukne&QG*u`-aReI&LOCx2zw>k!|< z+wt|OCjXB#OwM@5dqo(u_Rrk>JH=CqKTLkuFnj4{E=uKoMu+V?+>g6b6`y=>_cMQV zj)Bbh#Y}HH+b8}{Eyn^gfdj;j#>AWS>z8h?FN;uos^Y1r&|Vyt#g>OzLlWH{RET>{ zz5&S}Zu6-tbhO1Dvuvq5>pxQ=ooTXX7Gs#{Wvu2mi93Z@>lZP^M>}7nYQ%rvEzzer zwad#JDXlEwLsvG<D))2u_qf-5R?FJLJccjiRj@a4|+tC*tJ(7qgc@2FUR;FWD?U_#?3*!B4 z+G+DJ@D}UE3B5xQMaJ{zaS3v%GbjZTcNb zb68ujTnJK?tp3Utc@q;FQ@)3;?;KaFUEZk`Dj#Na2KINMpT5?0`+el|0muNOCc6bU z%4|XvjI}JcAk!DD)Q+P1@=FkvN3ZHC4PnpfKxA3P8u{*xckOZONWlwavW_##&#=V6^M@&xM6E%~_mA0yAn8F6Yc6}5K)XoPU$Vx#M81X@MS`u1N zH4VM%PeT7JH_oh}Jl?jcGw%p#Ue{1l<^b(^I#CK{gQ?BisS(qAj*&s>yUEmp#NOil zzf0Ay2D4in*(na$*6FT!H$R^hUY4Rszbay0A6updMLyUNA}oOGmQQF;5Xuu`R71@x$MF}2CXpSFj3 zwe^WW?{Zel;qK7IiK;&|IG5tTWxM6Q6^^QIjD;OyEW(qU2TrFoV36vWd?hInjyE|TjV~-H*ImM9-!!TDJ9uoCCz~I zB6;~ZWDC``7y@7JgtuWy6rBcs3Z^#~A0?yxRQ5xl7G_cKtf^{eW*(t@ z&Ma4qFxncb5a`}*UZ)X;D4w(G!6s`PU8e^p7Z|@AyKLZ@CON%5Te-K~gF2)oB?R~(jllFRwCtuzgeN<;h zcm#r!D^@C?&S=IzG=RI(AKbdV;3wcY#R6~A-P9Kt(0kpN`(L+DE7luM3@ldwR>tWT zsCOD zxn*pTXr8Ud94__SDgJj}t@rAr9DC(T719$2o`FsQlZnX=mN(2@NI>ve<)u<|o&{3) S*a|5r`GvMJnHERn4F3yNQR+Pa literal 0 HcmV?d00001 diff --git a/resources/contributors.txt b/resources/contributors.txt index d60742040a8..9d131c7b9ed 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -76,6 +76,7 @@ crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png SputNikPlop | https://github.com/SputNikPlop | fraxx | https://github.com/fraxxio | :/avatars/fraxx.png KleberPF | https://github.com/KleberPF | +nealxm | https://github.com/nealxm | :/avatars/nealxm.png # If you are a contributor add yourself above this line From 4adc5be4d2843aab34c603377ff85efabb7c7c6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:31:48 +0200 Subject: [PATCH 024/105] chore(deps): bump DoozyX/clang-format-lint-action from 0.16.2 to 0.17 (#5320) Bumps [DoozyX/clang-format-lint-action](https://github.com/doozyx/clang-format-lint-action) from 0.16.2 to 0.17. - [Release notes](https://github.com/doozyx/clang-format-lint-action/releases) - [Commits](https://github.com/doozyx/clang-format-lint-action/compare/v0.16.2...v0.17) --- updated-dependencies: - dependency-name: DoozyX/clang-format-lint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-formatting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 9a3a36685ad..4ae1f1134da 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -26,7 +26,7 @@ jobs: run: sudo apt-get -y install dos2unix - name: Check formatting - uses: DoozyX/clang-format-lint-action@v0.16.2 + uses: DoozyX/clang-format-lint-action@v0.17 with: source: "./src ./tests/src ./benchmarks/src ./mocks/include" extensions: "hpp,cpp" From e6bf503594da6bd02085d72d3924313cdd830e35 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:30:33 -0700 Subject: [PATCH 025/105] feat: include duration in more multi month gifts (#5319) * feat: include duration in more multi month gifts * chore: update sample data * chore: update changelog * push more sample data events to my channel * feat: use nicer display name for anon gifters --------- Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 50 +++++++++++++++++----- src/util/SampleData.cpp | 9 ++-- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 990cca03d71..0edadcdb769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Report sub duration for more multi-month gift cases. (#5319) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index afda22592f2..21a52ef1de9 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -518,8 +518,7 @@ std::vector parseUserNoticeMessage(Channel *channel, { messageText = "Announcement"; } - else if (msgType == "subgift" && - ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); monthsIt != tags.end()) @@ -528,13 +527,29 @@ std::vector parseUserNoticeMessage(Channel *channel, if (months > 1) { auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); messageText = - QString("An anonymous user gifted %1 months of a Tier " - "%2 sub to %3!") - .arg(QString::number(months), + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), plan.isEmpty() ? '1' : plan.at(0), tags.value("msg-param-recipient-display-name") .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } } } } @@ -1032,8 +1047,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } - else if (msgType == "subgift" && - ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()) + else if (msgType == "subgift") { if (auto monthsIt = tags.find("msg-param-gift-months"); monthsIt != tags.end()) @@ -1042,13 +1056,29 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, if (months > 1) { auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); messageText = - QString("An anonymous user gifted %1 months of a Tier " - "%2 sub to %3!") - .arg(QString::number(months), + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), plan.isEmpty() ? '1' : plan.at(0), tags.value("msg-param-recipient-display-name") .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } } } } diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 2c5b7ca4f8e..0b976f1900c 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -64,17 +64,20 @@ const QStringList &getSampleCheerMessages() const QStringList &getSampleSubMessages() { static QStringList list{ - R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=1337;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", + R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=11148817;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", R"(@badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=89614178;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\sof\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\sgifted\sa\sTier\s1\ssub\sto\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=13405587;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada)", // hyperbolicxd gifted a sub to quote_if_nam R"(@badges=subscriber/0,premium/1;color=#00FF7F;display-name=hyperbolicxd;emotes=;id=b20ef4fe-cba8-41d0-a371-6327651dc9cc;login=hyperbolicxd;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=quote_if_nam;msg-param-recipient-id=217259245;msg-param-recipient-user-name=quote_if_nam;msg-param-sender-count=1;msg-param-sub-plan-name=Channel\sSubscription\s(nymn_hs);msg-param-sub-plan=1000;room-id=62300805;subscriber=1;system-msg=hyperbolicxd\sgifted\sa\sTier\s1\ssub\sto\squote_if_nam!\sThis\sis\stheir\sfirst\sGift\sSub\sin\sthe\schannel!;tmi-sent-ts=1528190938558;turbo=0;user-id=111534250;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", // multi-month sub gift - R"(@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\sSubscription\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\sgifted\s6\smonths\sof\sTier\s1\sto\skimmi_tm.\sThey've\sgifted\s334\smonths\sin\sthe\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #mxddy)", + R"(@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\sSubscription\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\sgifted\s6\smonths\sof\sTier\s1\sto\skimmi_tm.\sThey've\sgifted\s334\smonths\sin\sthe\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #pajlada)", // multi-month anon sub gift - R"(@msg-param-goal-user-contributions=1;system-msg=An\sanonymous\suser\sgifted\sa\sTier\s1\ssub\sto\sMohammadrezaDH!\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\slee\sgoal\s:-) :tmi.twitch.tv USERNOTICE #jmarianne)", + R"(@msg-param-goal-user-contributions=1;system-msg=An\sanonymous\suser\sgifted\sa\sTier\s1\ssub\sto\sMohammadrezaDH!\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\slee\sgoal\s:-) :tmi.twitch.tv USERNOTICE #pajlada)", + + // multi-month sub gift by broadcaster + R"(@user-id=35759863;msg-param-origin-id=2862055070165643340;display-name=Lucidfoxx;id=eeb3cdb8-337c-413a-9521-3a884ff78754;msg-param-gift-months=12;msg-param-sub-plan=1000;vip=0;emotes=;badges=broadcaster/1,subscriber/3042,partner/1;msg-param-recipient-user-name=ogprodigy;msg-param-recipient-id=53888434;badge-info=subscriber/71;room-id=35759863;msg-param-recipient-display-name=OGprodigy;msg-param-sub-plan-name=Silver\sPackage;subscriber=1;system-msg=Lucidfoxx\sgifted\sa\sTier\s1\ssub\sto\sOGprodigy!;login=lucidfoxx;msg-param-sender-count=0;user-type=;mod=0;flags=;rm-received-ts=1712803947891;color=#EB078D;msg-param-months=15;tmi-sent-ts=1712803947773;msg-id=subgift :tmi.twitch.tv USERNOTICE #pajlada)", // first time sub R"(@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\sjust\ssubscribed\swith\sTwitch\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", From 1ca77a1e8417ce8c81ff36543ed7e99f42376b60 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 12 Apr 2024 23:05:47 +0200 Subject: [PATCH 026/105] Add context menu entry to toggle offline tabs (#5318) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/widgets/Notebook.cpp | 82 ++++++++++++++++++++++++++++++++++++++-- src/widgets/Notebook.hpp | 10 ++++- src/widgets/Window.cpp | 17 +-------- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0edadcdb769..cafe3e3e1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index c16181f8072..7fdff5495cf 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -33,7 +33,6 @@ namespace chatterino { Notebook::Notebook(QWidget *parent) : BaseWidget(parent) - , menu_(this) , addButton_(new NotebookButton(this)) { this->addButton_->setIcon(NotebookButton::Icon::Plus); @@ -81,8 +80,6 @@ Notebook::Notebook(QWidget *parent) << "Notebook must be created within a BaseWindow"; } - this->addNotebookActionsToMenu(&this->menu_); - // Manually resize the add button so the initial paint uses the correct // width when computing the maximum width occupied per column in vertical // tab rendering. @@ -1125,7 +1122,14 @@ void Notebook::mousePressEvent(QMouseEvent *event) switch (event->button()) { case Qt::RightButton: { - this->menu_.popup(event->globalPos() + QPoint(0, 8)); + event->accept(); + + if (!this->menu_) + { + this->menu_ = new QMenu(this); + this->addNotebookActionsToMenu(this->menu_); + } + this->menu_->popup(event->globalPos() + QPoint(0, 8)); } break; default:; @@ -1294,6 +1298,10 @@ SplitNotebook::SplitNotebook(Window *parent) this->addCustomButtons(); } + this->toggleOfflineTabsAction_ = new QAction({}, this); + QObject::connect(this->toggleOfflineTabsAction_, &QAction::triggered, this, + &SplitNotebook::toggleOfflineTabs); + getSettings()->tabVisibility.connect( [this](int val, auto) { auto visibility = NotebookTabVisibility(val); @@ -1307,12 +1315,17 @@ SplitNotebook::SplitNotebook(Window *parent) this->setTabVisibilityFilter([](const NotebookTab *tab) { return tab->isLive(); }); + this->toggleOfflineTabsAction_->setText("Show all tabs"); break; case NotebookTabVisibility::AllTabs: default: this->setTabVisibilityFilter(nullptr); + this->toggleOfflineTabsAction_->setText( + "Show live tabs only"); break; } + + this->updateToggleOfflineTabsHotkey(visibility); }, this->signalHolder_, true); @@ -1365,6 +1378,31 @@ SplitNotebook::SplitNotebook(Window *parent) }); } +void SplitNotebook::toggleOfflineTabs() +{ + if (!this->getShowTabs()) + { + // Tabs are currently hidden, so the intention is to show + // tabs again before enabling the live only setting + this->setShowTabs(true); + getSettings()->tabVisibility.setValue(NotebookTabVisibility::LiveOnly); + } + else + { + getSettings()->tabVisibility.setValue( + getSettings()->tabVisibility.getEnum() == + NotebookTabVisibility::LiveOnly + ? NotebookTabVisibility::AllTabs + : NotebookTabVisibility::LiveOnly); + } +} + +void SplitNotebook::addNotebookActionsToMenu(QMenu *menu) +{ + Notebook::addNotebookActionsToMenu(menu); + menu->addAction(this->toggleOfflineTabsAction_); +} + void SplitNotebook::showEvent(QShowEvent * /*event*/) { if (auto *page = this->getSelectedPage()) @@ -1442,6 +1480,42 @@ void SplitNotebook::addCustomButtons() this->updateStreamerModeIcon(); } +void SplitNotebook::updateToggleOfflineTabsHotkey( + NotebookTabVisibility newTabVisibility) +{ + auto *hotkeys = getIApp()->getHotkeys(); + auto getKeySequence = [&](auto argument) { + return hotkeys->getDisplaySequence(HotkeyCategory::Window, + "setTabVisibility", {{argument}}); + }; + + auto toggleSeq = getKeySequence("toggleLiveOnly"); + + switch (newTabVisibility) + { + case NotebookTabVisibility::AllTabs: + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("liveOnly"); + } + break; + + case NotebookTabVisibility::LiveOnly: + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("toggle"); + + if (toggleSeq.isEmpty()) + { + toggleSeq = getKeySequence("on"); + } + } + break; + } + + this->toggleOfflineTabsAction_->setShortcut(toggleSeq); +} + void SplitNotebook::updateStreamerModeIcon() { if (this->streamerModeIcon_ == nullptr) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 9aa694c66d6..ac0162c4283 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -118,7 +118,7 @@ class Notebook : public BaseWidget bool isNotebookLayoutLocked() const; void setLockNotebookLayout(bool value); - void addNotebookActionsToMenu(QMenu *menu); + virtual void addNotebookActionsToMenu(QMenu *menu); // Update layout and tab visibility void refresh(); @@ -182,7 +182,7 @@ class Notebook : public BaseWidget size_t visibleButtonCount() const; QList items_; - QMenu menu_; + QMenu *menu_ = nullptr; QWidget *selectedPage_ = nullptr; NotebookButton *addButton_; @@ -215,6 +215,9 @@ class SplitNotebook : public Notebook void select(QWidget *page, bool focusPage = true) override; void themeChangedEvent() override; + void addNotebookActionsToMenu(QMenu *menu) override; + void toggleOfflineTabs(); + protected: void showEvent(QShowEvent *event) override; @@ -223,6 +226,9 @@ class SplitNotebook : public Notebook pajlada::Signals::SignalHolder signalHolder_; + QAction *toggleOfflineTabsAction_; + void updateToggleOfflineTabsHotkey(NotebookTabVisibility newTabVisibility); + // Main window on Windows has basically a duplicate of this in Window NotebookButton *streamerModeIcon_{}; void updateStreamerModeIcon(); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 7106f872fcf..a9b0995ddfb 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -659,22 +659,7 @@ void Window::addShortcuts() } else if (arg == "toggleLiveOnly") { - if (!this->notebook_->getShowTabs()) - { - // Tabs are currently hidden, so the intention is to show - // tabs again before enabling the live only setting - this->notebook_->setShowTabs(true); - getSettings()->tabVisibility.setValue( - NotebookTabVisibility::LiveOnly); - } - else - { - getSettings()->tabVisibility.setValue( - getSettings()->tabVisibility.getEnum() == - NotebookTabVisibility::LiveOnly - ? NotebookTabVisibility::AllTabs - : NotebookTabVisibility::LiveOnly); - } + this->notebook_->toggleOfflineTabs(); } else { From f4e950ea0b06665c33c2b6ecc9b720daf3104276 Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 12 Apr 2024 23:48:08 +0200 Subject: [PATCH 027/105] Fix Wayland image upload crash if confirmation dialog is enabled (#5314) --- CHANGELOG.md | 1 + src/singletons/ImageUploader.cpp | 145 ++++++++++++++++++------------- src/singletons/ImageUploader.hpp | 10 ++- src/widgets/splits/Split.cpp | 20 ++++- 4 files changed, 111 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cafe3e3e1cd..3ed8fc68d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) +- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/singletons/ImageUploader.cpp b/src/singletons/ImageUploader.cpp index f8ad53d2e1e..3926df8f5e1 100644 --- a/src/singletons/ImageUploader.cpp +++ b/src/singletons/ImageUploader.cpp @@ -5,6 +5,7 @@ #include "common/network/NetworkRequest.hpp" #include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" +#include "debug/Benchmark.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Paths.hpp" @@ -21,6 +22,8 @@ #include #include +#include + #define UPLOAD_DELAY 2000 // Delay between uploads in milliseconds @@ -195,6 +198,11 @@ void ImageUploader::handleFailedUpload(const NetworkResult &result, } channel->addMessage(makeSystemMessage(errorMessage)); + // NOTE: We abort any future uploads on failure. Should this be handled differently? + while (!this->uploadQueue_.empty()) + { + this->uploadQueue_.pop(); + } this->uploadMutex_.unlock(); } @@ -248,22 +256,20 @@ void ImageUploader::handleSuccessfulUpload(const NetworkResult &result, this->logToFile(originalFilePath, link, deletionLink, channel); } -void ImageUploader::upload(const QMimeData *source, ChannelPtr channel, - QPointer outputTextEdit) +std::pair, QString> ImageUploader::getImages( + const QMimeData *source) const { - if (!this->uploadMutex_.tryLock()) - { - channel->addMessage(makeSystemMessage( - QString("Please wait until the upload finishes."))); - return; - } + BenchmarkGuard benchmarkGuard("ImageUploader::getImages"); - channel->addMessage(makeSystemMessage(QString("Started upload..."))); - auto tryUploadFromUrls = [&]() -> bool { + auto tryUploadFromUrls = + [&]() -> std::pair, QString> { if (!source->hasUrls()) { - return false; + return {{}, {}}; } + + std::queue images; + auto mimeDb = QMimeDatabase(); // This path gets chosen when files are copied from a file manager, like explorer.exe, caja. // Each entry in source->urls() is a QUrl pointing to a file that was copied. @@ -273,101 +279,118 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel, QMimeType mime = mimeDb.mimeTypeForUrl(path); if (mime.name().startsWith("image") && !mime.inherits("image/gif")) { - channel->addMessage(makeSystemMessage( - QString("Uploading image: %1").arg(localPath))); QImage img = QImage(localPath); if (img.isNull()) { - channel->addMessage( - makeSystemMessage(QString("Couldn't load image :("))); - return false; + return {{}, "Couldn't load image :("}; } auto imageData = convertToPng(img); - if (imageData) - { - RawImageData data = {*imageData, "png", localPath}; - this->uploadQueue_.push(data); - } - else + if (!imageData) { - channel->addMessage(makeSystemMessage( + return { + {}, QString("Cannot upload file: %1. Couldn't convert " "image to png.") - .arg(localPath))); - return false; + .arg(localPath), + }; } + images.push({*imageData, "png", localPath}); } else if (mime.inherits("image/gif")) { - channel->addMessage(makeSystemMessage( - QString("Uploading GIF: %1").arg(localPath))); QFile file(localPath); bool isOkay = file.open(QIODevice::ReadOnly); if (!isOkay) { - channel->addMessage( - makeSystemMessage(QString("Failed to open file. :("))); - return false; + return {{}, "Failed to open file :("}; } // file.readAll() => might be a bit big but it /should/ work - RawImageData data = {file.readAll(), "gif", localPath}; - this->uploadQueue_.push(data); + images.push({file.readAll(), "gif", localPath}); file.close(); } } - if (!this->uploadQueue_.empty()) - { - this->sendImageUploadRequest(this->uploadQueue_.front(), channel, - outputTextEdit); - this->uploadQueue_.pop(); - return true; - } - return false; + + return {images, {}}; }; - auto tryUploadDirectly = [&]() -> bool { + auto tryUploadDirectly = + [&]() -> std::pair, QString> { + std::queue images; + if (source->hasFormat("image/png")) { // the path to file is not present every time, thus the filePath is empty - this->sendImageUploadRequest({source->data("image/png"), "png", ""}, - channel, outputTextEdit); - return true; + images.push({source->data("image/png"), "png", ""}); + return {images, {}}; } + if (source->hasFormat("image/jpeg")) { - this->sendImageUploadRequest( - {source->data("image/jpeg"), "jpeg", ""}, channel, - outputTextEdit); - return true; + images.push({source->data("image/jpeg"), "jpeg", ""}); + return {images, {}}; } + if (source->hasFormat("image/gif")) { - this->sendImageUploadRequest({source->data("image/gif"), "gif", ""}, - channel, outputTextEdit); - return true; + images.push({source->data("image/gif"), "gif", ""}); + return {images, {}}; } + // not PNG, try loading it into QImage and save it to a PNG. auto image = qvariant_cast(source->imageData()); auto imageData = convertToPng(image); if (imageData) { - sendImageUploadRequest({*imageData, "png", ""}, channel, - outputTextEdit); - return true; + images.push({*imageData, "png", ""}); + return {images, {}}; } + // No direct upload happenned - channel->addMessage(makeSystemMessage( - QString("Cannot upload file, failed to convert to png."))); - return false; + return {{}, "Cannot upload file, failed to convert to png."}; }; - if (!tryUploadFromUrls() && !tryUploadDirectly()) + const auto [urlImageData, urlError] = tryUploadFromUrls(); + + if (!urlImageData.empty()) { - channel->addMessage( - makeSystemMessage(QString("Cannot upload file from clipboard."))); - this->uploadMutex_.unlock(); + return {urlImageData, {}}; } + + const auto [directImageData, directError] = tryUploadDirectly(); + if (!directImageData.empty()) + { + return {directImageData, {}}; + } + + return { + {}, + // TODO: verify that this looks ok xd + urlError + directError, + }; +} + +void ImageUploader::upload(std::queue images, ChannelPtr channel, + QPointer outputTextEdit) +{ + BenchmarkGuard benchmarkGuard("upload"); + if (!this->uploadMutex_.tryLock()) + { + channel->addMessage(makeSystemMessage( + QString("Please wait until the upload finishes."))); + return; + } + + assert(!images.empty()); + assert(this->uploadQueue_.empty()); + + std::swap(this->uploadQueue_, images); + + channel->addMessage(makeSystemMessage("Started upload...")); + + this->sendImageUploadRequest(this->uploadQueue_.front(), std::move(channel), + std::move(outputTextEdit)); + this->uploadQueue_.pop(); } } // namespace chatterino diff --git a/src/singletons/ImageUploader.hpp b/src/singletons/ImageUploader.hpp index 260180583d7..41f4c8b6050 100644 --- a/src/singletons/ImageUploader.hpp +++ b/src/singletons/ImageUploader.hpp @@ -25,8 +25,16 @@ struct RawImageData { class ImageUploader final : public Singleton { public: + /** + * Tries to get the image(s) from the given QMimeData + * + * If no images were found, the second value in the pair will contain an error message + */ + std::pair, QString> getImages( + const QMimeData *source) const; + void save() override; - void upload(const QMimeData *source, ChannelPtr channel, + void upload(std::queue images, ChannelPtr channel, QPointer outputTextEdit); private: diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 9e31263197f..1e4c228ef9c 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -380,12 +380,26 @@ Split::Split(QWidget *parent) // this connection can be ignored since the SplitInput is owned by this Split std::ignore = this->input_->ui_.textEdit->imagePasted.connect( - [this](const QMimeData *source) { + [this](const QMimeData *original) { if (!getSettings()->imageUploaderEnabled) { return; } + auto channel = this->getChannel(); + auto *imageUploader = getIApp()->getImageUploader(); + + auto [images, imageProcessError] = + imageUploader->getImages(original); + if (images.empty()) + { + channel->addMessage(makeSystemMessage( + QString( + "An error occurred trying to process your image: %1") + .arg(imageProcessError))); + return; + } + if (getSettings()->askOnImageUpload.getValue()) { QMessageBox msgBox(this->window()); @@ -427,9 +441,9 @@ Split::Split(QWidget *parent) return; } } + QPointer edit = this->input_->ui_.textEdit; - getIApp()->getImageUploader()->upload(source, this->getChannel(), - edit); + imageUploader->upload(std::move(images), channel, edit); }); getSettings()->imageUploaderEnabled.connect( From bf8266e9b3fbf3e68166bdc90038cf848a9f4bf9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 13 Apr 2024 09:01:20 +0200 Subject: [PATCH 028/105] fix: improve error messaging when pgrep fails streamer mode detection (#5321) When launching pgrep times out, a timeout message is now posted instead of the default "pgrep is not installed" error. --- CHANGELOG.md | 1 + src/singletons/StreamerMode.cpp | 51 ++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed8fc68d22..803cdb60edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Added context menu action to toggle visibility of offline tabs. (#5318) - Minor: Report sub duration for more multi-month gift cases. (#5319) +- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) - Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index 7ee4fa5884b..4eda9e3bd47 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -51,6 +51,9 @@ const QStringList &broadcastingBinaries() bool isBroadcasterSoftwareActive() { #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + static bool shouldShowTimeoutWarning = true; + static bool shouldShowWarning = true; + QProcess p; p.start("pgrep", {"-xi", broadcastingBinaries().join("|")}, QIODevice::NotOpen); @@ -62,20 +65,46 @@ bool isBroadcasterSoftwareActive() // Fallback to false and showing a warning - static bool shouldShowWarning = true; - if (shouldShowWarning) + switch (p.error()) { - shouldShowWarning = false; - - postToThread([] { - getApp()->twitch->addGlobalSystemMessage( - "Streamer Mode is set to Automatic, but pgrep is missing. " - "Install it to fix the issue or set Streamer Mode to " - "Enabled or Disabled in the Settings."); - }); + case QProcess::Timedout: { + qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; + if (shouldShowTimeoutWarning) + { + shouldShowTimeoutWarning = false; + + postToThread([] { + getApp()->twitch->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep timed " + "out. This can happen if your system lagged at the " + "wrong moment. If Streamer Mode continues to not work, " + "you can manually set it to Enabled or Disabled in the " + "Settings."); + }); + } + } + break; + + default: { + qCWarning(chatterinoStreamerMode) + << "pgrep execution failed:" << p.error(); + + if (shouldShowWarning) + { + shouldShowWarning = false; + + postToThread([] { + getApp()->twitch->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep is " + "missing. " + "Install it to fix the issue or set Streamer Mode to " + "Enabled or Disabled in the Settings."); + }); + } + } + break; } - qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; return false; #elif defined(Q_OS_WIN) if (!IsWindowsVistaOrGreater()) From 7285f08a043e726ce04857cb27b86b69d80b0280 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 13 Apr 2024 10:18:34 +0200 Subject: [PATCH 029/105] Fixed links having `http://` added to the beginning in certain cases. (#5323) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803cdb60edd..3dfc30ca472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) +- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) ## 2.5.0-beta.1 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index cc71558f65e..45b8cafda9c 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -635,7 +635,7 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) auto textColor = MessageColor(MessageColor::Link); auto *el = this->emplace( LinkElement::Parsed{.lowercase = lowercaseLinkString, - .original = matchedLink}, + .original = origLink}, MessageElementFlag::Text, textColor); el->setLink({Link::Url, matchedLink}); getIApp()->getLinkResolver()->resolve(el->linkInfo()); From c391ff9740bdc2f01ed6c82516340f1a97195cca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 23:05:11 -0400 Subject: [PATCH 030/105] chore(deps): bump lib/settings from `ceac9c7` to `70fbc72` (#5325) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index ceac9c7e97d..70fbc7236aa 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit ceac9c7e97d2d2b97f40ecd0b421e358d7525cbc +Subproject commit 70fbc7236aa8bcf5db4748e7f56dad132d6fd402 From b391f18177a0b3870e6a1cd4d48f7c1a622ec87d Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 17 Apr 2024 19:08:47 +0200 Subject: [PATCH 031/105] fix: set maximum of scrollbar after filtering (#5329) --- CHANGELOG.md | 1 + src/messages/LimitedQueue.hpp | 16 ++++++++-------- src/widgets/helper/ChannelView.cpp | 7 +++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfc30ca472..2cf91610335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) ## 2.5.0-beta.1 diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 62fd025278d..e06e5a0f2b4 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -24,14 +24,6 @@ class LimitedQueue private: /// Property Accessors - /** - * @brief Return the limit of the internal buffer - */ - [[nodiscard]] size_t limit() const - { - return this->limit_; - } - /** * @brief Return the amount of space left in the buffer * @@ -43,6 +35,14 @@ class LimitedQueue } public: + /** + * @brief Return the limit of the queue + */ + [[nodiscard]] size_t limit() const + { + return this->limit_; + } + /** * @brief Return true if the buffer is empty */ diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c76373d5679..c5a679727fe 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -982,8 +982,7 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) // and the ui. auto snapshot = underlyingChannel->getMessageSnapshot(); - this->scrollBar_->setMaximum(qreal(snapshot.size())); - + size_t nMessagesAdded = 0; for (const auto &msg : snapshot) { if (!this->shouldIncludeMessage(msg)) @@ -1007,12 +1006,16 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->messages_.pushBack(messageLayout); this->channel_->addMessage(msg); + nMessagesAdded++; if (this->showScrollbarHighlights()) { this->scrollBar_->addHighlight(msg->getScrollBarHighlight()); } } + this->scrollBar_->setMaximum( + static_cast(std::min(nMessagesAdded, this->messages_.limit()))); + // // Standard channel connections // From 86a27823a27fc7bc174edfe92b9d3b3601addfc5 Mon Sep 17 00:00:00 2001 From: nerix Date: Thu, 18 Apr 2024 16:24:12 +0200 Subject: [PATCH 032/105] fix: don't change the topmost value of child windows (#5330) --- CHANGELOG.md | 1 + src/widgets/BaseWindow.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf91610335..b3d60bdc772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) - Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) ## 2.5.0-beta.1 diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index a61322fcec0..81ed90b28be 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -263,6 +263,13 @@ void BaseWindow::tryApplyTopMost() } this->waitingForTopMost_ = false; + if (this->parent()) + { + // Don't change the topmost value of child windows. This would apply + // to the top-level window too. + return; + } + ::SetWindowPos(*hwnd, this->isTopMost_ ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } From 7c97e6bcc748755ee55447096ebc657be11b4789 Mon Sep 17 00:00:00 2001 From: Maverick Date: Thu, 18 Apr 2024 17:49:50 +0200 Subject: [PATCH 033/105] Change order of query parameters of Twitch Player URLs. (#5326) This ensures that it doesn't "fake redirect". Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/common/Common.hpp | 3 +++ src/providers/twitch/TwitchChannel.cpp | 3 +-- src/singletons/Toasts.cpp | 6 +++--- src/widgets/splits/Split.cpp | 3 +-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d60bdc772..8c19a0de8db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) - Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) ## 2.5.0-beta.1 diff --git a/src/common/Common.hpp b/src/common/Common.hpp index 35b8efb1cc3..8d6097473bc 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -14,6 +14,9 @@ namespace chatterino { +const inline auto TWITCH_PLAYER_URL = + QStringLiteral("https://player.twitch.tv/?channel=%1&parent=twitch.tv"); + enum class HighlightState { None, Highlighted, diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 39350b69c12..c478b919133 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -84,8 +84,7 @@ TwitchChannel::TwitchChannel(const QString &name) , nameOptions{name, name, name} , subscriptionUrl_("https://www.twitch.tv/subs/" + name) , channelUrl_("https://twitch.tv/" + name) - , popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" + - name) + , popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name)) , bttvEmotes_(std::make_shared()) , ffzEmotes_(std::make_shared()) , seventvEmotes_(std::make_shared()) diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index 51dbf468010..3ca5b6e0c8a 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -1,6 +1,7 @@ #include "Toasts.hpp" #include "Application.hpp" +#include "common/Common.hpp" #include "common/Literals.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" @@ -177,9 +178,8 @@ class CustomHandler : public WinToastLib::IWinToastHandler case ToastReaction::OpenInPlayer: if (platform_ == Platform::Twitch) { - QDesktopServices::openUrl(QUrl( - u"https://player.twitch.tv/?parent=twitch.tv&channel=" % - channelName_)); + QDesktopServices::openUrl( + QUrl(TWITCH_PLAYER_URL.arg(channelName_))); } break; case ToastReaction::OpenInStreamlink: { diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 1e4c228ef9c..81b0e2f65cf 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -836,8 +836,7 @@ void Split::openChannelInBrowserPlayer(ChannelPtr channel) if (auto *twitchChannel = dynamic_cast(channel.get())) { QDesktopServices::openUrl( - "https://player.twitch.tv/?parent=twitch.tv&channel=" + - twitchChannel->getName()); + QUrl(TWITCH_PLAYER_URL.arg(twitchChannel->getName()))); } } From 992ea88884052058c709986e199fa23ebac2b8a9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 20 Apr 2024 11:14:23 +0200 Subject: [PATCH 034/105] fix: Remove "Show chatter list" entry from split header menu for non-mods (#5336) --- CHANGELOG.md | 1 + src/widgets/splits/SplitHeader.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c19a0de8db..f76498cc51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) - Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) - Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) - Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) ## 2.5.0-beta.1 diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index ed4a59b185e..6b7021798ea 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -525,9 +525,12 @@ std::unique_ptr SplitHeader::createMainMenu() if (twitchChannel) { - moreMenu->addAction( - "Show chatter list", this->split_, &Split::showChatterList, - h->getDisplaySequence(HotkeyCategory::Split, "openViewerList")); + if (twitchChannel->hasModRights()) + { + moreMenu->addAction( + "Show chatter list", this->split_, &Split::showChatterList, + h->getDisplaySequence(HotkeyCategory::Split, "openViewerList")); + } moreMenu->addAction("Subscribe", this->split_, &Split::openSubPage); From 48cbb7f8d1053d713e4a734675be8f30e59fdf06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 09:40:35 +0000 Subject: [PATCH 035/105] chore(deps): bump lib/lua/src from `e288c5a` to `0897c0a` (#5316) --- lib/lua/src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lua/src b/lib/lua/src index e288c5a9188..0897c0a4289 160000 --- a/lib/lua/src +++ b/lib/lua/src @@ -1 +1 @@ -Subproject commit e288c5a91883793d14ed9e9d93464f6ee0b08915 +Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60 From 3aead09339ffff0fbfd9f8a7a5ea9eb551ad8fb8 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 11:17:25 +0200 Subject: [PATCH 036/105] Release v2.5.0 (#5337) --- CHANGELOG.md | 25 +++++++++---------- .../com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76498cc51f..3fed1f7512a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,7 @@ ## Unversioned -- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) -- Minor: Report sub duration for more multi-month gift cases. (#5319) -- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) -- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) -- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) -- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) -- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) -- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) -- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) -- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) -- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) - -## 2.5.0-beta.1 +## 2.5.0 - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) - Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) @@ -68,6 +56,16 @@ - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Minor: Changed the layout of the about page. (#5287) - Minor: Add duration to multi-month anon sub gift messages. (#5293) +- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) +- Minor: Report sub duration for more multi-month gift cases. (#5319) +- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) +- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) +- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) +- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) +- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) +- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) @@ -138,6 +136,7 @@ - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) - Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) - Bugfix: Fixed highlights triggering for ignored users in announcements. (#5295) +- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) - 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) diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 5f76c9f6d42..69d1b9289d6 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0 + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 3673e5b23f1..b58af4fbd44 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.5.0-beta.1" +#define CHATTERINO_VERSION "2.5.0" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From 1a04bda56ba82422f37ce09d68453efb3b6932cd Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 18:30:31 +0200 Subject: [PATCH 037/105] fix: use openssl3 on qt6 windows builds (#5340) regression since 2.4.6 --- .github/workflows/build.yml | 9 ++------- .github/workflows/test-windows.yml | 8 ++------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1581eddf07..0a7408d2285 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,6 +139,8 @@ jobs: C2_PLUGINS: ${{ matrix.plugins }} C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }} C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} steps: - uses: actions/checkout@v4 @@ -188,13 +190,6 @@ jobs: if: startsWith(matrix.os, 'windows') uses: ilammy/msvc-dev-cmd@v1.13.0 - - name: Setup conan variables (Windows) - if: startsWith(matrix.os, 'windows') - run: | - "C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV" - "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" - shell: powershell - - name: Setup sccache (Windows) # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 12ddb164cd5..f1c76e8cbb1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,6 +32,8 @@ jobs: env: C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }} QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} steps: - name: Enable plugin support @@ -65,12 +67,6 @@ jobs: - name: Enable Developer Command Prompt uses: ilammy/msvc-dev-cmd@v1.13.0 - - name: Setup conan variables - if: startsWith(matrix.os, 'windows') - run: | - "C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV" - "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" - - name: Setup sccache # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 From dfa929e20736e3da93a97e12de288974dcc69871 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 21 Apr 2024 21:24:11 +0200 Subject: [PATCH 038/105] fix: use the full url when resolving (#5345) --- CHANGELOG.md | 2 ++ src/common/LinkParser.hpp | 19 +++++++++++++++++++ src/messages/MessageBuilder.cpp | 9 ++++----- src/messages/MessageElement.cpp | 7 ++++--- src/messages/MessageElement.hpp | 5 ++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fed1f7512a..d335d8274f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Bugfix: Fixed links without a protocol not being clickable. (#5345) + ## 2.5.0 - Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 16bfe235e52..9d5e10cfb68 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -12,9 +12,28 @@ struct ParsedLink { #else using StringView = QStringRef; #endif + /// The parsed protocol of the link. Can be empty. + /// + /// https://www.forsen.tv/commands + /// ^------^ StringView protocol; + + /// The parsed host of the link. Can not be empty. + /// + /// https://www.forsen.tv/commands + /// ^-----------^ StringView host; + + /// The remainder of the link. Can be empty. + /// + /// https://www.forsen.tv/commands + /// ^-------^ StringView rest; + + /// The original unparsed link. + /// + /// https://www.forsen.tv/commands + /// ^----------------------------^ QString source; }; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 45b8cafda9c..e1719454971 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -617,16 +617,16 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) { QString lowercaseLinkString; QString origLink = parsedLink.source; - QString matchedLink; + QString fullUrl; if (parsedLink.protocol.isNull()) { - matchedLink = QStringLiteral("http://") + parsedLink.source; + fullUrl = QStringLiteral("http://") + parsedLink.source; } else { lowercaseLinkString += parsedLink.protocol; - matchedLink = parsedLink.source; + fullUrl = parsedLink.source; } lowercaseLinkString += parsedLink.host.toString().toLower(); @@ -636,8 +636,7 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink) auto *el = this->emplace( LinkElement::Parsed{.lowercase = lowercaseLinkString, .original = origLink}, - MessageElementFlag::Text, textColor); - el->setLink({Link::Url, matchedLink}); + fullUrl, MessageElementFlag::Text, textColor); getIApp()->getLinkResolver()->resolve(el->linkInfo()); } diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 56d1e2ed3d3..b11e82be197 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -679,10 +679,11 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container, } } -LinkElement::LinkElement(const Parsed &parsed, MessageElementFlags flags, - const MessageColor &color, FontStyle style) +LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, const MessageColor &color, + FontStyle style) : TextElement({}, flags, color, style) - , linkInfo_(parsed.original) + , linkInfo_(fullUrl) , lowercase_({parsed.lowercase}) , original_({parsed.original}) { diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 68f90e9b57e..b57bab752c4 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -272,7 +272,10 @@ class LinkElement : public TextElement QString original; }; - LinkElement(const Parsed &parsed, MessageElementFlags flags, + /// @param parsed The link as it appeared in the message + /// @param fullUrl A full URL (notably with a protocol) + LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, const MessageColor &color = MessageColor::Text, FontStyle style = FontStyle::ChatMedium); ~LinkElement() override = default; From 4a1ce2a3b322c6e16a6b96245608829fe9bb8d17 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 21 Apr 2024 22:52:44 +0200 Subject: [PATCH 039/105] refactor: clean up toStdString usages in tests (#5346) * tests: Add QString/QStringView << operators for std::ostream This makes it easier to print QString/QStringView's in ASSERT_EQ outputs * tests: clean up toStdString usages * fix: use QByteArray.toStdString instead --- tests/src/Emojis.cpp | 6 +++--- tests/src/LinkParser.cpp | 14 ++++++++------ tests/src/TestHelpers.hpp | 22 ++++++++++++++++++++++ tests/src/TwitchMessageBuilder.cpp | 20 ++++++++++---------- tests/src/XDGHelper.cpp | 7 ++++--- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/tests/src/Emojis.cpp b/tests/src/Emojis.cpp index 0f6cf67628e..141a64afbf7 100644 --- a/tests/src/Emojis.cpp +++ b/tests/src/Emojis.cpp @@ -1,6 +1,7 @@ #include "providers/emoji/Emojis.hpp" #include "common/Literals.hpp" +#include "TestHelpers.hpp" #include #include @@ -53,7 +54,7 @@ TEST(Emojis, ShortcodeParsing) } EXPECT_EQ(output, test.expectedOutput) - << "Input " << test.input.toStdString() << " failed"; + << "Input " << test.input << " failed"; } } @@ -165,8 +166,7 @@ TEST(Emojis, Parse) // can't use EXPECT_EQ because EmotePtr can't be printed if (output != test.expectedOutput) { - EXPECT_TRUE(false) - << "Input " << test.input.toStdString() << " failed"; + EXPECT_TRUE(false) << "Input " << test.input << " failed"; } } } diff --git a/tests/src/LinkParser.cpp b/tests/src/LinkParser.cpp index 0931ef85917..cce5c8c6c52 100644 --- a/tests/src/LinkParser.cpp +++ b/tests/src/LinkParser.cpp @@ -1,5 +1,7 @@ #include "common/LinkParser.hpp" +#include "TestHelpers.hpp" + #include #include #include @@ -15,13 +17,13 @@ struct Case { { auto input = this->protocol + this->host + this->rest; LinkParser p(input); - ASSERT_TRUE(p.result().has_value()) << input.toStdString(); + ASSERT_TRUE(p.result().has_value()) << input; const auto &r = *p.result(); ASSERT_EQ(r.source, input); - ASSERT_EQ(r.protocol, this->protocol) << this->protocol.toStdString(); - ASSERT_EQ(r.host, this->host) << this->host.toStdString(); - ASSERT_EQ(r.rest, this->rest) << this->rest.toStdString(); + ASSERT_EQ(r.protocol, this->protocol) << this->protocol; + ASSERT_EQ(r.host, this->host) << this->host; + ASSERT_EQ(r.rest, this->rest) << this->rest; } }; @@ -126,7 +128,7 @@ TEST(LinkParser, doesntParseInvalidIpv4Links) for (const auto &input : inputs) { LinkParser p(input); - ASSERT_FALSE(p.result().has_value()) << input.toStdString(); + ASSERT_FALSE(p.result().has_value()) << input; } } @@ -170,6 +172,6 @@ TEST(LinkParser, doesntParseInvalidLinks) for (const auto &input : inputs) { LinkParser p(input); - ASSERT_FALSE(p.result().has_value()) << input.toStdString(); + ASSERT_FALSE(p.result().has_value()) << input; } } diff --git a/tests/src/TestHelpers.hpp b/tests/src/TestHelpers.hpp index 05190e0dadc..30a2d0b1a25 100644 --- a/tests/src/TestHelpers.hpp +++ b/tests/src/TestHelpers.hpp @@ -1,6 +1,10 @@ #pragma once +#include +#include + #include +#include template class ReceivedMessage @@ -42,3 +46,21 @@ class ReceivedMessage return &this->t; } }; + +inline std::ostream &operator<<(std::ostream &os, const QStringView &str) +{ + os << qUtf8Printable(str.toString()); + return os; +} + +inline std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) +{ + os << bytes.toStdString(); + return os; +} + +inline std::ostream &operator<<(std::ostream &os, const QString &str) +{ + os << qUtf8Printable(str); + return os; +} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index 7b6b42c33f0..77ddcdf4659 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -15,6 +15,7 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Emotes.hpp" +#include "TestHelpers.hpp" #include #include @@ -147,7 +148,7 @@ TEST(TwitchMessageBuilder, CommaSeparatedListTagParsing) auto output = TwitchMessageBuilder::slashKeyValue(test.input); EXPECT_EQ(output, test.expectedOutput) - << "Input " << test.input.toStdString() << " failed"; + << "Input " << test.input << " failed"; } } @@ -230,12 +231,12 @@ TEST(TwitchMessageBuilder, BadgeInfoParsing) auto outputBadgeInfo = TwitchMessageBuilder::parseBadgeInfoTag(privmsg->tags()); EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo) - << "Input for badgeInfo " << test.input.toStdString() << " failed"; + << "Input for badgeInfo " << test.input << " failed"; auto outputBadges = SharedMessageBuilder::parseBadgeTag(privmsg->tags()); EXPECT_EQ(outputBadges, test.expectedBadges) - << "Input for badges " << test.input.toStdString() << " failed"; + << "Input for badges " << test.input << " failed"; delete privmsg; } @@ -413,8 +414,7 @@ TEST_F(TestTwitchMessageBuilder, ParseTwitchEmotes) privmsg->tags(), originalMessage, 0); EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes) - << "Input for twitch emotes " << test.input.toStdString() - << " failed"; + << "Input for twitch emotes " << test.input << " failed"; delete privmsg; } @@ -617,11 +617,11 @@ TEST_F(TestTwitchMessageBuilder, IgnoresReplace) emotes); EXPECT_EQ(message, test.expectedMessage) - << "Message not equal for input '" << test.input.toStdString() - << "' - expected: '" << test.expectedMessage.toStdString() - << "' got: '" << message.toStdString() << "'"; + << "Message not equal for input '" << test.input + << "' - expected: '" << test.expectedMessage << "' got: '" + << message << "'"; EXPECT_EQ(emotes, test.expectedTwitchEmotes) - << "Twitch emotes not equal for input '" << test.input.toStdString() - << "' and output '" << message.toStdString() << "'"; + << "Twitch emotes not equal for input '" << test.input + << "' and output '" << message << "'"; } } diff --git a/tests/src/XDGHelper.cpp b/tests/src/XDGHelper.cpp index a8bcac80182..3926d21d926 100644 --- a/tests/src/XDGHelper.cpp +++ b/tests/src/XDGHelper.cpp @@ -1,5 +1,7 @@ #include "util/XDGHelper.hpp" +#include "TestHelpers.hpp" + #include #include @@ -57,9 +59,8 @@ TEST(XDGHelper, ParseDesktopExecProgram) auto output = parseDesktopExecProgram(test.input); EXPECT_EQ(output, test.expectedOutput) - << "Input '" << test.input.toStdString() << "' failed. Expected '" - << test.expectedOutput.toStdString() << "' but got '" - << output.toStdString() << "'"; + << "Input '" << test.input << "' failed. Expected '" + << test.expectedOutput << "' but got '" << output << "'"; } } From 1caf7ca4d6f10d520c5711389b930a6c25eaca66 Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Mon, 22 Apr 2024 01:07:35 -0400 Subject: [PATCH 040/105] fix: warning in homebrew workflow (#5347) --- .github/workflows/homebrew.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index b455baaec44..6da0e71d3c0 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -26,4 +26,5 @@ jobs: echo "Running bump-cask-pr for cask '$C2_CASK_NAME' and version '$C2_TAGGED_VERSION'" C2_TAGGED_VERSION_STRIPPED="${C2_TAGGED_VERSION:1}" echo "Stripped version: '$C2_TAGGED_VERSION_STRIPPED'" + brew developer on brew bump-cask-pr --version "$C2_TAGGED_VERSION_STRIPPED" "$C2_CASK_NAME" From 58a930d28cdfe0daa32c5ecdd1aa2045f55f3f26 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 26 Apr 2024 15:15:09 +0200 Subject: [PATCH 041/105] ci: pin to macos-13 (#5362) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a7408d2285..53d9558b725 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,7 +112,7 @@ jobs: matrix: include: # macOS - - os: macos-latest + - os: macos-13 qt-version: 5.15.2 force-lto: false plugins: false From eafcb941f57011358d63c76de6bee38ca1ba97ec Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 28 Apr 2024 11:36:58 +0200 Subject: [PATCH 042/105] Release v2.5.1 (#5364) --- .CI/chatterino-installer.iss | 2 +- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 +- resources/com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss index fddd668f98c..1f9816a2978 100644 --- a/.CI/chatterino-installer.iss +++ b/.CI/chatterino-installer.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Chatterino" -#define MyAppVersion "2.5.0" +#define MyAppVersion "2.5.1" #define MyAppPublisher "Chatterino Team" #define MyAppURL "https://www.chatterino.com" #define MyAppExeName "chatterino.exe" diff --git a/CHANGELOG.md b/CHANGELOG.md index d335d8274f6..07df030e064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.5.1 + - Bugfix: Fixed links without a protocol not being clickable. (#5345) ## 2.5.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a08cf79ee1..fdcb7075b9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ if(BUILD_BENCHMARKS) endif() project(chatterino - VERSION 2.5.0 + VERSION 2.5.1 DESCRIPTION "Chat client for twitch.tv" HOMEPAGE_URL "https://chatterino.com/" ) diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 69d1b9289d6..a2d09fecbdc 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -34,6 +34,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.1 + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index b58af4fbd44..fbe536a69b8 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.5.0" +#define CHATTERINO_VERSION "2.5.1" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From 80cf1e533c5efd5ffe661e92fb8b7dc4748738d1 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 28 Apr 2024 13:36:01 +0200 Subject: [PATCH 043/105] ci: add workflow for WinGet (#5365) --- .github/workflows/winget.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/winget.yml diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 00000000000..7e8a5091a70 --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,14 @@ +name: Publish to WinGet +on: + release: + types: [released] +jobs: + publish: + runs-on: windows-latest + if: ${{ startsWith(github.event.release.tag_name, 'v') }} + steps: + - uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: ChatterinoTeam.Chatterino + installers-regex: ^Chatterino.Installer.exe$ + token: ${{ secrets.WINGET_TOKEN }} From b5066881f947d3f3cdc13eda3a16ed7d84ce5471 Mon Sep 17 00:00:00 2001 From: fossdd Date: Tue, 30 Apr 2024 21:14:50 +0000 Subject: [PATCH 044/105] docs: add `https://`to github URL (#5368) --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index f9bc5798a9a..47ac1b202ae 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,4 +7,4 @@ docker run --network=host --detach ghcr.io/chatterino/twitch-pubsub-server-test: docker run -p 9051:80 --detach kennethreitz/httpbin ``` -If you're unable to use docker, you can use [httpbox](github.com/kevinastone/httpbox) (`httpbox --port 9051`) and [Chatterino/twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) manually. +If you're unable to use docker, you can use [httpbox](https://github.com/kevinastone/httpbox) (`httpbox --port 9051`) and [Chatterino/twitch-pubsub-server-test](https://github.com/Chatterino/twitch-pubsub-server-test/releases/latest) manually. From 85cb2a1f3cea22b6aea66f211140da05f448cef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 10:11:56 +0000 Subject: [PATCH 045/105] chore(deps): bump hendrikmuhs/ccache-action from 1.2.12 to 1.2.13 (#5370) Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.12 to 1.2.13. - [Release notes](https://github.com/hendrikmuhs/ccache-action/releases) - [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.12...v1.2.13) --- updated-dependencies: - dependency-name: hendrikmuhs/ccache-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pajlada --- .github/workflows/build.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53d9558b725..3e42b5dd6f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,7 +192,7 @@ jobs: - name: Setup sccache (Windows) # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.12 + uses: hendrikmuhs/ccache-action@v1.2.13 if: startsWith(matrix.os, 'windows') with: variant: sccache diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f1c76e8cbb1..0b73ee84f30 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,7 +69,7 @@ jobs: - name: Setup sccache # sccache v0.7.4 - uses: hendrikmuhs/ccache-action@v1.2.12 + uses: hendrikmuhs/ccache-action@v1.2.13 with: variant: sccache # only save on the default (master) branch From a88a2ac65ca283a2c68db7479800f870a610f421 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 4 May 2024 12:39:14 +0200 Subject: [PATCH 046/105] build: add doxygen target (#5377) Co-authored-by: Nerixyz --- CHANGELOG.md | 2 ++ CMakeLists.txt | 1 + src/CMakeLists.txt | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07df030e064..0a5d65fa27b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Dev: Add doxygen build target. (#5377) + ## 2.5.1 - Bugfix: Fixed links without a protocol not being clickable. (#5345) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdcb7075b9f..eec6b7e1c8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ find_package(PajladaSerialize REQUIRED) find_package(PajladaSignals REQUIRED) find_package(LRUCache REQUIRED) find_package(MagicEnum REQUIRED) +find_package(Doxygen) if (USE_SYSTEM_PAJLADA_SETTINGS) find_package(PajladaSettings REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb64bcf31f0..301808906cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1141,3 +1141,14 @@ if(NOT CHATTERINO_UPDATER) message(STATUS "Disabling the updater.") target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_DISABLE_UPDATER) endif() + +if (DOXYGEN_FOUND) + message(STATUS "Doxygen found, adding doxygen target") + # output will be in docs/html + set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs") + + doxygen_add_docs( + doxygen + ${CMAKE_CURRENT_LIST_DIR} + ) +endif () From 401feac0aadffc381d2921a2cae3dfa12104aa1d Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 5 May 2024 15:01:07 +0200 Subject: [PATCH 047/105] tests: better test initializing allowing for better printing (#5379) Co-authored-by: Nerixyz --- CHANGELOG.md | 1 + tests/CMakeLists.txt | 3 +- tests/src/AccessGuard.cpp | 2 +- tests/src/BasicPubSub.cpp | 2 +- tests/src/BttvLiveUpdates.cpp | 3 +- tests/src/ChannelChatters.cpp | 2 +- tests/src/ChatterSet.cpp | 3 +- tests/src/Emojis.cpp | 3 +- tests/src/ExponentialBackoff.cpp | 2 +- tests/src/Filters.cpp | 43 +++++++++---------- tests/src/FormatTime.cpp | 13 +++--- tests/src/Helpers.cpp | 15 ++++--- tests/src/HighlightController.cpp | 9 ++-- tests/src/HighlightPhrase.cpp | 2 +- tests/src/Hotkeys.cpp | 3 +- tests/src/InputCompletion.cpp | 4 +- tests/src/IrcHelpers.cpp | 7 ++-- tests/src/LimitedQueue.cpp | 2 +- tests/src/LinkInfo.cpp | 3 +- tests/src/LinkParser.cpp | 3 +- tests/src/Literals.cpp | 2 +- tests/src/MessageLayout.cpp | 2 +- tests/src/NetworkCommon.cpp | 2 +- tests/src/NetworkRequest.cpp | 2 +- tests/src/NetworkResult.cpp | 2 +- tests/src/NotebookTab.cpp | 3 +- tests/src/QMagicEnum.cpp | 3 +- tests/src/RatelimitBucket.cpp | 3 +- tests/src/Selection.cpp | 2 +- tests/src/SeventvEventAPI.cpp | 2 +- tests/src/SplitInput.cpp | 8 ++-- tests/src/Test.cpp | 42 +++++++++++++++++++ tests/src/Test.hpp | 22 ++++++++++ tests/src/TestHelpers.hpp | 66 ------------------------------ tests/src/TwitchMessageBuilder.cpp | 3 +- tests/src/TwitchPubSubClient.cpp | 45 +++++++++++++++++++- tests/src/Updates.cpp | 2 +- tests/src/UtilTwitch.cpp | 32 ++++++--------- tests/src/XDGDesktopFile.cpp | 3 +- tests/src/XDGHelper.cpp | 3 +- tests/src/main.cpp | 2 +- 41 files changed, 198 insertions(+), 178 deletions(-) create mode 100644 tests/src/Test.cpp create mode 100644 tests/src/Test.hpp delete mode 100644 tests/src/TestHelpers.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5d65fa27b..56d2b0054d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Dev: Add doxygen build target. (#5377) +- Dev: Make printing of strings in tests easier. (#5379) ## 2.5.1 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fb5730048b8..bd35b79dee9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,8 @@ option(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN "Use public httpbin for testing networ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp ${CMAKE_CURRENT_LIST_DIR}/resources/test-resources.qrc - ${CMAKE_CURRENT_LIST_DIR}/src/TestHelpers.hpp + ${CMAKE_CURRENT_LIST_DIR}/src/Test.hpp + ${CMAKE_CURRENT_LIST_DIR}/src/Test.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp ${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp diff --git a/tests/src/AccessGuard.cpp b/tests/src/AccessGuard.cpp index a0d1c6d3199..56cbc727fea 100644 --- a/tests/src/AccessGuard.cpp +++ b/tests/src/AccessGuard.cpp @@ -1,6 +1,6 @@ #include "common/UniqueAccess.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/BasicPubSub.cpp b/tests/src/BasicPubSub.cpp index dc277522028..6315970ff06 100644 --- a/tests/src/BasicPubSub.cpp +++ b/tests/src/BasicPubSub.cpp @@ -1,7 +1,7 @@ #include "providers/liveupdates/BasicPubSubClient.hpp" #include "providers/liveupdates/BasicPubSubManager.hpp" +#include "Test.hpp" -#include #include #include #include diff --git a/tests/src/BttvLiveUpdates.cpp b/tests/src/BttvLiveUpdates.cpp index 580f2e61f96..2d238f9b0b8 100644 --- a/tests/src/BttvLiveUpdates.cpp +++ b/tests/src/BttvLiveUpdates.cpp @@ -1,6 +1,7 @@ #include "providers/bttv/BttvLiveUpdates.hpp" -#include +#include "Test.hpp" + #include #include diff --git a/tests/src/ChannelChatters.cpp b/tests/src/ChannelChatters.cpp index c665836bb38..79711ce15af 100644 --- a/tests/src/ChannelChatters.cpp +++ b/tests/src/ChannelChatters.cpp @@ -1,8 +1,8 @@ #include "common/ChannelChatters.hpp" #include "mocks/Channel.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/ChatterSet.cpp b/tests/src/ChatterSet.cpp index ac5b81ee926..57a67a77113 100644 --- a/tests/src/ChatterSet.cpp +++ b/tests/src/ChatterSet.cpp @@ -1,6 +1,7 @@ #include "common/ChatterSet.hpp" -#include +#include "Test.hpp" + #include TEST(ChatterSet, insert) diff --git a/tests/src/Emojis.cpp b/tests/src/Emojis.cpp index 141a64afbf7..42df110a426 100644 --- a/tests/src/Emojis.cpp +++ b/tests/src/Emojis.cpp @@ -1,9 +1,8 @@ #include "providers/emoji/Emojis.hpp" #include "common/Literals.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/ExponentialBackoff.cpp b/tests/src/ExponentialBackoff.cpp index 2a4259744a1..7099ea08a17 100644 --- a/tests/src/ExponentialBackoff.cpp +++ b/tests/src/ExponentialBackoff.cpp @@ -1,6 +1,6 @@ #include "util/ExponentialBackoff.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index ff1b0590212..89c0a510f88 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -13,8 +13,8 @@ #include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Emotes.hpp" +#include "Test.hpp" -#include #include #include @@ -101,7 +101,7 @@ namespace chatterino::filters { std::ostream &operator<<(std::ostream &os, Type t) { - os << qUtf8Printable(typeToString(t)); + os << typeToString(t); return os; } @@ -138,8 +138,8 @@ TEST(Filters, Validity) auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); EXPECT_EQ(isValid, expected) - << "Filter::fromString( " << qUtf8Printable(input) - << " ) should be " << (expected ? "valid" : "invalid"); + << "Filter::fromString( " << input << " ) should be " + << (expected ? "valid" : "invalid"); } } @@ -168,15 +168,14 @@ TEST(Filters, TypeSynthesis) { auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); - ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input) - << " ) is invalid"; + ASSERT_TRUE(isValid) + << "Filter::fromString( " << input << " ) is invalid"; auto filter = std::move(std::get(filterResult)); T type = filter.returnType(); EXPECT_EQ(type, expected) - << "Filter{ " << qUtf8Printable(input) << " } has type " << type - << " instead of " << expected << ".\nDebug: " - << qUtf8Printable(filter.debugString(typingContext)); + << "Filter{ " << input << " } has type " << type << " instead of " + << expected << ".\nDebug: " << filter.debugString(typingContext); } } @@ -244,17 +243,16 @@ TEST(Filters, Evaluation) { auto filterResult = Filter::fromString(input); bool isValid = std::holds_alternative(filterResult); - ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input) - << " ) is invalid"; + ASSERT_TRUE(isValid) + << "Filter::fromString( " << input << " ) is invalid"; auto filter = std::move(std::get(filterResult)); auto result = filter.execute(contextMap); EXPECT_EQ(result, expected) - << "Filter{ " << qUtf8Printable(input) << " } evaluated to " - << qUtf8Printable(result.toString()) << " instead of " - << qUtf8Printable(expected.toString()) << ".\nDebug: " - << qUtf8Printable(filter.debugString(typingContext)); + << "Filter{ " << input << " } evaluated to " << result.toString() + << " instead of " << expected.toString() + << ".\nDebug: " << filter.debugString(typingContext); } } @@ -354,20 +352,17 @@ TEST_F(FiltersF, ExpressionDebug) { const auto filterResult = Filter::fromString(input); const auto *filter = std::get_if(&filterResult); - EXPECT_NE(filter, nullptr) - << "Filter::fromString(" << qUtf8Printable(input) - << ") did not build a proper filter"; + EXPECT_NE(filter, nullptr) << "Filter::fromString(" << input + << ") did not build a proper filter"; const auto actualDebugString = filter->debugString(typingContext); EXPECT_EQ(actualDebugString, debugString) - << "filter->debugString() on '" << qUtf8Printable(input) - << "' should be '" << qUtf8Printable(debugString) << "', but got '" - << qUtf8Printable(actualDebugString) << "'"; + << "filter->debugString() on '" << input << "' should be '" + << debugString << "', but got '" << actualDebugString << "'"; const auto actualFilterString = filter->filterString(); EXPECT_EQ(actualFilterString, filterString) - << "filter->filterString() on '" << qUtf8Printable(input) - << "' should be '" << qUtf8Printable(filterString) << "', but got '" - << qUtf8Printable(actualFilterString) << "'"; + << "filter->filterString() on '" << input << "' should be '" + << filterString << "', but got '" << actualFilterString << "'"; } } diff --git a/tests/src/FormatTime.cpp b/tests/src/FormatTime.cpp index bc15f44efe6..6fe82ab9a8c 100644 --- a/tests/src/FormatTime.cpp +++ b/tests/src/FormatTime.cpp @@ -1,6 +1,6 @@ #include "util/FormatTime.hpp" -#include +#include "Test.hpp" #include @@ -62,8 +62,8 @@ TEST(FormatTime, Int) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << input - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } @@ -130,8 +130,8 @@ TEST(FormatTime, QString) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << qUtf8Printable(input) - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } @@ -202,7 +202,6 @@ TEST(FormatTime, chrono) const auto actual = formatTime(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " did not match expected value " - << qUtf8Printable(expected); + << actual << " did not match expected value " << expected; } } diff --git a/tests/src/Helpers.cpp b/tests/src/Helpers.cpp index d6a74fec037..c615167cff7 100644 --- a/tests/src/Helpers.cpp +++ b/tests/src/Helpers.cpp @@ -1,6 +1,6 @@ #include "util/Helpers.hpp" -#include +#include "Test.hpp" using namespace chatterino; using namespace _helpers_internal; @@ -275,8 +275,8 @@ TEST(Helpers, skipSpace) const auto actual = skipSpace(makeView(c.input), c.startIdx); EXPECT_EQ(actual, c.expected) - << actual << " (" << qUtf8Printable(c.input) - << ") did not match expected value " << c.expected; + << actual << " (" << c.input << ") did not match expected value " + << c.expected; } } @@ -418,14 +418,13 @@ TEST(Helpers, findUnitMultiplierToSec) if (c.expectedMultiplier == bad) { - EXPECT_FALSE(actual.second) << qUtf8Printable(c.input); + EXPECT_FALSE(actual.second) << c.input; } else { EXPECT_TRUE(pos == c.expectedEndPos && actual.second && actual.first == c.expectedMultiplier) - << qUtf8Printable(c.input) - << ": Expected(end: " << c.expectedEndPos + << c.input << ": Expected(end: " << c.expectedEndPos << ", mult: " << c.expectedMultiplier << ") Actual(end: " << pos << ", mult: " << actual.first << ")"; } @@ -503,7 +502,7 @@ TEST(Helpers, parseDurationToSeconds) const auto actual = parseDurationToSeconds(c.input, c.noUnitMultiplier); EXPECT_EQ(actual, c.output) - << actual << " (" << qUtf8Printable(c.input) - << ") did not match expected value " << c.output; + << actual << " (" << c.input << ") did not match expected value " + << c.output; } } diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index a45bbf98c02..090acf37b15 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -10,9 +10,8 @@ #include "providers/twitch/TwitchBadge.hpp" // for Badge #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include -#include #include #include #include @@ -216,11 +215,9 @@ class HighlightControllerTest : public ::testing::Test input.originalMessage, input.flags); EXPECT_EQ(isMatch, expected.state) - << qUtf8Printable(input.senderName) << ": " - << qUtf8Printable(input.originalMessage); + << input.senderName << ": " << input.originalMessage; EXPECT_EQ(matchResult, expected.result) - << qUtf8Printable(input.senderName) << ": " - << qUtf8Printable(input.originalMessage); + << input.senderName << ": " << input.originalMessage; } } diff --git a/tests/src/HighlightPhrase.cpp b/tests/src/HighlightPhrase.cpp index 374670b03d3..2ec2530f0f5 100644 --- a/tests/src/HighlightPhrase.cpp +++ b/tests/src/HighlightPhrase.cpp @@ -1,6 +1,6 @@ #include "controllers/highlights/HighlightPhrase.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/Hotkeys.cpp b/tests/src/Hotkeys.cpp index ebbfe50297f..7c3d8d10fe6 100644 --- a/tests/src/Hotkeys.cpp +++ b/tests/src/Hotkeys.cpp @@ -1,6 +1,5 @@ #include "controllers/hotkeys/HotkeyHelpers.hpp" - -#include +#include "Test.hpp" #include diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp index 86003543871..22c42b31cb1 100644 --- a/tests/src/InputCompletion.cpp +++ b/tests/src/InputCompletion.cpp @@ -12,9 +12,9 @@ #include "singletons/Emotes.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" #include "widgets/splits/InputCompletionPopup.hpp" -#include #include #include #include @@ -224,7 +224,7 @@ void containsRoughly(std::span span, std::set values) } } - ASSERT_TRUE(found) << qPrintable(v) << " was not found in the span"; + ASSERT_TRUE(found) << v << " was not found in the span"; } } diff --git a/tests/src/IrcHelpers.cpp b/tests/src/IrcHelpers.cpp index acae81c3537..d210b14e003 100644 --- a/tests/src/IrcHelpers.cpp +++ b/tests/src/IrcHelpers.cpp @@ -1,6 +1,7 @@ #include "util/IrcHelpers.hpp" -#include +#include "Test.hpp" + #include #include #include @@ -55,7 +56,7 @@ TEST(IrcHelpers, ParseTagString) const auto actual = parseTagString(input); EXPECT_EQ(actual, expected) - << qUtf8Printable(actual) << " (" << qUtf8Printable(input) - << ") did not match expected value " << qUtf8Printable(expected); + << actual << " (" << input << ") did not match expected value " + << expected; } } diff --git a/tests/src/LimitedQueue.cpp b/tests/src/LimitedQueue.cpp index 39a8bba86ac..0a94ea92874 100644 --- a/tests/src/LimitedQueue.cpp +++ b/tests/src/LimitedQueue.cpp @@ -1,6 +1,6 @@ #include "messages/LimitedQueue.hpp" -#include +#include "Test.hpp" #include diff --git a/tests/src/LinkInfo.cpp b/tests/src/LinkInfo.cpp index 91f065035ce..a06a78c0f9c 100644 --- a/tests/src/LinkInfo.cpp +++ b/tests/src/LinkInfo.cpp @@ -2,8 +2,7 @@ #include "common/Literals.hpp" #include "SignalSpy.hpp" - -#include +#include "Test.hpp" using namespace chatterino; using namespace literals; diff --git a/tests/src/LinkParser.cpp b/tests/src/LinkParser.cpp index cce5c8c6c52..9d964ce1542 100644 --- a/tests/src/LinkParser.cpp +++ b/tests/src/LinkParser.cpp @@ -1,8 +1,7 @@ #include "common/LinkParser.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/Literals.cpp b/tests/src/Literals.cpp index 77607b73977..17d459b1431 100644 --- a/tests/src/Literals.cpp +++ b/tests/src/Literals.cpp @@ -1,6 +1,6 @@ #include "common/Literals.hpp" -#include +#include "Test.hpp" using namespace chatterino::literals; diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index 9ce0c7f21e8..ab9a294c9b8 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -10,8 +10,8 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/NetworkCommon.cpp b/tests/src/NetworkCommon.cpp index 481f951aee5..9beab8da68c 100644 --- a/tests/src/NetworkCommon.cpp +++ b/tests/src/NetworkCommon.cpp @@ -1,6 +1,6 @@ #include "common/network/NetworkCommon.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index 2f6b8102f96..ca723481efe 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -2,8 +2,8 @@ #include "common/network/NetworkManager.hpp" #include "common/network/NetworkResult.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/NetworkResult.cpp b/tests/src/NetworkResult.cpp index 72a2ca771f0..4bf2366a3bf 100644 --- a/tests/src/NetworkResult.cpp +++ b/tests/src/NetworkResult.cpp @@ -1,6 +1,6 @@ #include "common/network/NetworkResult.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/NotebookTab.cpp b/tests/src/NotebookTab.cpp index 2ac4903f47e..36133b648c8 100644 --- a/tests/src/NotebookTab.cpp +++ b/tests/src/NotebookTab.cpp @@ -7,10 +7,9 @@ #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" +#include "Test.hpp" #include "widgets/Notebook.hpp" -#include -#include #include #include diff --git a/tests/src/QMagicEnum.cpp b/tests/src/QMagicEnum.cpp index 80c265efe24..6778427fe43 100644 --- a/tests/src/QMagicEnum.cpp +++ b/tests/src/QMagicEnum.cpp @@ -2,8 +2,7 @@ #include "common/FlagsEnum.hpp" #include "common/Literals.hpp" - -#include +#include "Test.hpp" using namespace chatterino; using namespace literals; diff --git a/tests/src/RatelimitBucket.cpp b/tests/src/RatelimitBucket.cpp index c92a42234b8..850f14c68cb 100644 --- a/tests/src/RatelimitBucket.cpp +++ b/tests/src/RatelimitBucket.cpp @@ -1,6 +1,7 @@ #include "util/RatelimitBucket.hpp" -#include +#include "Test.hpp" + #include #include #include diff --git a/tests/src/Selection.cpp b/tests/src/Selection.cpp index 1f1f4a62137..a904b076656 100644 --- a/tests/src/Selection.cpp +++ b/tests/src/Selection.cpp @@ -1,6 +1,6 @@ #include "messages/Selection.hpp" -#include +#include "Test.hpp" using namespace chatterino; diff --git a/tests/src/SeventvEventAPI.cpp b/tests/src/SeventvEventAPI.cpp index 780f90bac10..4e2d3228193 100644 --- a/tests/src/SeventvEventAPI.cpp +++ b/tests/src/SeventvEventAPI.cpp @@ -3,8 +3,8 @@ #include "providers/seventv/eventapi/Client.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Message.hpp" +#include "Test.hpp" -#include #include #include diff --git a/tests/src/SplitInput.cpp b/tests/src/SplitInput.cpp index ed092f94baf..d84da81188a 100644 --- a/tests/src/SplitInput.cpp +++ b/tests/src/SplitInput.cpp @@ -12,11 +12,10 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "Test.hpp" #include "widgets/Notebook.hpp" #include "widgets/splits/Split.hpp" -#include -#include #include #include @@ -110,9 +109,8 @@ TEST_P(SplitInputTest, Reply) auto reply = MessagePtr(message); this->input.setReply(reply); QString actual = this->input.getInputText(); - ASSERT_EQ(expected, actual) - << "Input text after setReply should be '" << qUtf8Printable(expected) - << "', but got '" << qUtf8Printable(actual) << "'"; + ASSERT_EQ(expected, actual) << "Input text after setReply should be '" + << expected << "', but got '" << actual << "'"; } INSTANTIATE_TEST_SUITE_P( diff --git a/tests/src/Test.cpp b/tests/src/Test.cpp new file mode 100644 index 00000000000..5f245f5d7f2 --- /dev/null +++ b/tests/src/Test.cpp @@ -0,0 +1,42 @@ +#include "Test.hpp" + +#include +#include + +std::ostream &operator<<(std::ostream &os, QStringView str) +{ + os << str.toString().toStdString(); + return os; +} + +std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) +{ + os << std::string_view{bytes.data(), static_cast(bytes.size())}; + return os; +} + +std::ostream &operator<<(std::ostream &os, const QString &str) +{ + os << str.toStdString(); + return os; +} + +// The PrintTo overloads use UniversalPrint to print strings in quotes. +// Even though this uses testing::internal, this is publically documented in +// gtest/gtest-printers.h. + +void PrintTo(const QByteArray &bytes, std::ostream *os) +{ + ::testing::internal::UniversalPrint(bytes.toStdString(), os); +} + +void PrintTo(QStringView str, std::ostream *os) +{ + ::testing::internal::UniversalPrint( + std::u16string{str.utf16(), static_cast(str.size())}, os); +} + +void PrintTo(const QString &str, std::ostream *os) +{ + ::testing::internal::UniversalPrint(str.toStdU16String(), os); +} diff --git a/tests/src/Test.hpp b/tests/src/Test.hpp new file mode 100644 index 00000000000..064f90c6d79 --- /dev/null +++ b/tests/src/Test.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include + +class QString; +class QStringView; +class QByteArray; + +// This file is included in all TUs in chatterino-test to avoid ODR violations. +std::ostream &operator<<(std::ostream &os, QStringView str); +std::ostream &operator<<(std::ostream &os, const QByteArray &bytes); +std::ostream &operator<<(std::ostream &os, const QString &str); + +// NOLINTBEGIN(readability-identifier-naming) +// PrintTo is used for naming parameterized tests, and is part of gtest +void PrintTo(const QByteArray &bytes, std::ostream *os); +void PrintTo(QStringView str, std::ostream *os); +void PrintTo(const QString &str, std::ostream *os); +// NOLINTEND(readability-identifier-naming) diff --git a/tests/src/TestHelpers.hpp b/tests/src/TestHelpers.hpp deleted file mode 100644 index 30a2d0b1a25..00000000000 --- a/tests/src/TestHelpers.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -template -class ReceivedMessage -{ - mutable std::mutex mutex; - - bool isSet{false}; - T t; - -public: - ReceivedMessage() = default; - - explicit operator bool() const - { - std::unique_lock lock(this->mutex); - - return this->isSet; - } - - ReceivedMessage &operator=(const T &newT) - { - std::unique_lock lock(this->mutex); - - this->isSet = true; - this->t = newT; - - return *this; - } - - bool operator==(const T &otherT) const - { - std::unique_lock lock(this->mutex); - - return this->t == otherT; - } - - const T *operator->() const - { - return &this->t; - } -}; - -inline std::ostream &operator<<(std::ostream &os, const QStringView &str) -{ - os << qUtf8Printable(str.toString()); - return os; -} - -inline std::ostream &operator<<(std::ostream &os, const QByteArray &bytes) -{ - os << bytes.toStdString(); - return os; -} - -inline std::ostream &operator<<(std::ostream &os, const QString &str) -{ - os << qUtf8Printable(str); - return os; -} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index 77ddcdf4659..d9d1d5a62f6 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -15,9 +15,8 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Emotes.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include #include diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index 30e02e56759..728b0e5bbcc 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -4,12 +4,12 @@ #include "providers/twitch/pubsubmessages/AutoMod.hpp" #include "providers/twitch/pubsubmessages/Whisper.hpp" #include "providers/twitch/TwitchAccount.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #include +#include #include using namespace chatterino; @@ -33,6 +33,47 @@ using namespace std::chrono_literals; #ifdef RUN_PUBSUB_TESTS +template +class ReceivedMessage +{ + mutable std::mutex mutex; + + bool isSet{false}; + T t; + +public: + ReceivedMessage() = default; + + explicit operator bool() const + { + std::unique_lock lock(this->mutex); + + return this->isSet; + } + + ReceivedMessage &operator=(const T &newT) + { + std::unique_lock lock(this->mutex); + + this->isSet = true; + this->t = newT; + + return *this; + } + + bool operator==(const T &otherT) const + { + std::unique_lock lock(this->mutex); + + return this->t == otherT; + } + + const T *operator->() const + { + return &this->t; + } +}; + class FTest : public PubSub { public: diff --git a/tests/src/Updates.cpp b/tests/src/Updates.cpp index da4762517a6..ce16f329f6a 100644 --- a/tests/src/Updates.cpp +++ b/tests/src/Updates.cpp @@ -1,8 +1,8 @@ #include "singletons/Updates.hpp" #include "common/Version.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/UtilTwitch.cpp b/tests/src/UtilTwitch.cpp index 6a0b58d9fa6..3a2a7b41ba5 100644 --- a/tests/src/UtilTwitch.cpp +++ b/tests/src/UtilTwitch.cpp @@ -1,6 +1,6 @@ +#include "Test.hpp" #include "util/Twitch.hpp" -#include #include #include #include @@ -72,9 +72,8 @@ TEST(UtilTwitch, StripUserName) stripUserName(userName); EXPECT_EQ(userName, expectedUserName) - << qUtf8Printable(userName) << " (" << qUtf8Printable(inputUserName) - << ") did not match expected value " - << qUtf8Printable(expectedUserName); + << userName << " (" << inputUserName + << ") did not match expected value " << expectedUserName; } } @@ -153,10 +152,8 @@ TEST(UtilTwitch, StripChannelName) stripChannelName(userName); EXPECT_EQ(userName, expectedChannelName) - << qUtf8Printable(userName) << " (" - << qUtf8Printable(inputChannelName) - << ") did not match expected value " - << qUtf8Printable(expectedChannelName); + << userName << " (" << inputChannelName + << ") did not match expected value " << expectedChannelName; } } @@ -259,14 +256,12 @@ TEST(UtilTwitch, ParseUserNameOrID) auto [actualUserName, actualUserID] = parseUserNameOrID(input); EXPECT_EQ(actualUserName, expectedUserName) - << "name " << qUtf8Printable(actualUserName) << " (" - << qUtf8Printable(input) << ") did not match expected value " - << qUtf8Printable(expectedUserName); + << "name " << actualUserName << " (" << input + << ") did not match expected value " << expectedUserName; EXPECT_EQ(actualUserID, expectedUserID) - << "id " << qUtf8Printable(actualUserID) << " (" - << qUtf8Printable(input) << ") did not match expected value " - << qUtf8Printable(expectedUserID); + << "id " << actualUserID << " (" << input + << ") did not match expected value " << expectedUserID; } } @@ -319,7 +314,7 @@ TEST(UtilTwitch, UserLoginRegexp) auto actual = regexp.match(inputUserLogin); EXPECT_EQ(match.hasMatch(), expectedMatch) - << qUtf8Printable(inputUserLogin) << " did not match as expected"; + << inputUserLogin << " did not match as expected"; } } @@ -371,7 +366,7 @@ TEST(UtilTwitch, UserNameRegexp) auto actual = regexp.match(inputUserLogin); EXPECT_EQ(match.hasMatch(), expectedMatch) - << qUtf8Printable(inputUserLogin) << " did not match as expected"; + << inputUserLogin << " did not match as expected"; } } @@ -405,8 +400,7 @@ TEST(UtilTwitch, CleanHelixColor) cleanHelixColorName(actualColor); EXPECT_EQ(actualColor, expectedColor) - << qUtf8Printable(inputColor) << " cleaned up to " - << qUtf8Printable(actualColor) << " instead of " - << qUtf8Printable(expectedColor); + << inputColor << " cleaned up to " << actualColor << " instead of " + << expectedColor; } } diff --git a/tests/src/XDGDesktopFile.cpp b/tests/src/XDGDesktopFile.cpp index bffe529aad2..69f4d370642 100644 --- a/tests/src/XDGDesktopFile.cpp +++ b/tests/src/XDGDesktopFile.cpp @@ -1,6 +1,7 @@ #include "util/XDGDesktopFile.hpp" -#include +#include "Test.hpp" + #include #if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) diff --git a/tests/src/XDGHelper.cpp b/tests/src/XDGHelper.cpp index 3926d21d926..3ab48daa3d5 100644 --- a/tests/src/XDGHelper.cpp +++ b/tests/src/XDGHelper.cpp @@ -1,8 +1,7 @@ #include "util/XDGHelper.hpp" -#include "TestHelpers.hpp" +#include "Test.hpp" -#include #include #if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 3b24a997885..6c82f632cb8 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,8 +1,8 @@ #include "common/network/NetworkManager.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include #include #include #include From 56fa973d7c1d8bb895b69e83942a9dd2ef6ba0cb Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 5 May 2024 19:37:22 +0200 Subject: [PATCH 048/105] fix: prefer reporting error over status for 200 OK (#5378) --- CHANGELOG.md | 1 + src/common/network/NetworkResult.cpp | 11 ++++++++++- tests/src/NetworkResult.cpp | 13 +++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d2b0054d0..eae3d6f30ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/common/network/NetworkResult.cpp b/src/common/network/NetworkResult.cpp index 177d2ae6f97..c6544eaad80 100644 --- a/src/common/network/NetworkResult.cpp +++ b/src/common/network/NetworkResult.cpp @@ -67,7 +67,9 @@ const QByteArray &NetworkResult::getData() const QString NetworkResult::formatError() const { - if (this->status_) + // Print the status for errors that mirror HTTP status codes (=0 || >99) + if (this->status_ && (this->error_ == QNetworkReply::NoError || + this->error_ > QNetworkReply::UnknownNetworkError)) { return QString::number(*this->status_); } @@ -77,6 +79,13 @@ QString NetworkResult::formatError() const this->error_); if (name == nullptr) { + if (this->status_) + { + return QStringLiteral("unknown error (status: %1, error: %2)") + .arg(QString::number(*this->status_), + QString::number(this->error_)); + } + return QStringLiteral("unknown error (%1)").arg(this->error_); } return name; diff --git a/tests/src/NetworkResult.cpp b/tests/src/NetworkResult.cpp index 4bf2366a3bf..4d4c574210d 100644 --- a/tests/src/NetworkResult.cpp +++ b/tests/src/NetworkResult.cpp @@ -37,12 +37,21 @@ TEST(NetworkResult, Errors) "RemoteHostClosedError"); // status code takes precedence - checkResult({Error::TimeoutError, 400, {}}, Error::TimeoutError, 400, - "400"); + checkResult({Error::InternalServerError, 400, {}}, + Error::InternalServerError, 400, "400"); + + // error takes precedence (1..=99) + checkResult({Error::BackgroundRequestNotAllowedError, 400, {}}, + Error::BackgroundRequestNotAllowedError, 400, + "BackgroundRequestNotAllowedError"); + checkResult({Error::UnknownNetworkError, 400, {}}, + Error::UnknownNetworkError, 400, "UnknownNetworkError"); } TEST(NetworkResult, InvalidError) { checkResult({static_cast(-1), {}, {}}, static_cast(-1), std::nullopt, "unknown error (-1)"); + checkResult({static_cast(-1), 42, {}}, static_cast(-1), 42, + "unknown error (status: 42, error: -1)"); } From a43c4f371b3bbacd16a5a59da71c48b6f347d190 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 21:13:02 -0400 Subject: [PATCH 049/105] chore(deps): bump lib/settings from `70fbc72` to `03e8af1` (#5382) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index 70fbc7236aa..03e8af1934e 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 70fbc7236aa8bcf5db4748e7f56dad132d6fd402 +Subproject commit 03e8af1934e6151edfe8a44dfb025b747a31acdf From 321d881bfe2f7cc77ac64fc843484d5e9f170c43 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 6 May 2024 17:03:17 +0200 Subject: [PATCH 050/105] Release plugins alpha (#5288) --- .github/workflows/build.yml | 6 +++--- BUILDING_ON_FREEBSD.md | 2 +- BUILDING_ON_LINUX.md | 2 +- BUILDING_ON_MAC.md | 2 +- BUILDING_ON_WINDOWS.md | 1 + BUILDING_ON_WINDOWS_WITH_VCPKG.md | 1 + CHANGELOG.md | 1 + CMakeLists.txt | 2 +- src/widgets/settingspages/PluginsPage.cpp | 15 +++++++++++++++ 9 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e42b5dd6f3..5a4d077174d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,21 +115,21 @@ jobs: - os: macos-13 qt-version: 5.15.2 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: false # Windows - os: windows-latest qt-version: 6.5.0 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: false # Windows 7/8 - os: windows-latest qt-version: 5.15.2 force-lto: false - plugins: false + plugins: true skip-artifact: false skip-crashpad: true diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md index 8a1deeebae8..26e751c939e 100644 --- a/BUILDING_ON_FREEBSD.md +++ b/BUILDING_ON_FREEBSD.md @@ -15,7 +15,7 @@ FreeBSD 13.0-CURRENT. mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh cmake .. ``` diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index b901e8e6d7e..3aa2df4a83f 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -51,7 +51,7 @@ nix-shell -p openssl boost qt6.full pkg-config cmake mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh cmake -DBUILD_WITH_QT6=ON -DBUILD_WITH_QTKEYCHAIN=OFF .. ``` diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index 78f94c7e9ec..1c8fc38a1cf 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -20,7 +20,7 @@ Local dev machines for testing are available on Apple Silicon on macOS 13. 1. Go to the project directory where you cloned Chatterino2 & its submodules 1. Create a build directory and go into it: `mkdir build && cd build` -1. Run CMake: +1. Run CMake. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. `cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..` 1. Build: `make` diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index dc66d65c434..42d71cc5133 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -118,6 +118,7 @@ nmake ``` To build a debug build, you'll also need to add the `-s compiler.runtime_type=Debug` flag to the `conan install` invocation. See [this StackOverflow post](https://stackoverflow.com/questions/59828611/windeployqt-doesnt-deploy-qwindowsd-dll-for-a-debug-application/75607313#75607313) +To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake` command. #### Deploying Qt libraries diff --git a/BUILDING_ON_WINDOWS_WITH_VCPKG.md b/BUILDING_ON_WINDOWS_WITH_VCPKG.md index b998094311c..ec961572413 100644 --- a/BUILDING_ON_WINDOWS_WITH_VCPKG.md +++ b/BUILDING_ON_WINDOWS_WITH_VCPKG.md @@ -50,4 +50,5 @@ This will require more than 30GB of free space on your hard drive. cmake --build . --parallel --config Release ``` When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain. + To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake -B build` command. 1. Run `.\bin\chatterino2.exe` diff --git a/CHANGELOG.md b/CHANGELOG.md index eae3d6f30ee..4bd5f9a4731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Major: Release plugins alpha. (#5288) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/CMakeLists.txt b/CMakeLists.txt index eec6b7e1c8d..6fb323286d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF) option(BUILD_TRANSLATIONS "" OFF) option(BUILD_SHARED_LIBS "" OFF) option(CHATTERINO_LTO "Enable LTO for all targets" OFF) -option(CHATTERINO_PLUGINS "Enable EXPERIMENTAL plugin support in Chatterino" OFF) +option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" OFF) option(CHATTERINO_UPDATER "Enable update checks" ON) mark_as_advanced(CHATTERINO_UPDATER) diff --git a/src/widgets/settingspages/PluginsPage.cpp b/src/widgets/settingspages/PluginsPage.cpp index aad35f7513c..05c80a37c7c 100644 --- a/src/widgets/settingspages/PluginsPage.cpp +++ b/src/widgets/settingspages/PluginsPage.cpp @@ -37,6 +37,21 @@ PluginsPage::PluginsPage() auto group = layout.emplace("General plugin settings"); this->generalGroup = group.getElement(); auto groupLayout = group.setLayoutType(); + auto *scaryLabel = new QLabel( + "Plugins can expand functionality of " + "Chatterino. They can be made in Lua. This functionality is " + "still in public alpha stage. Use ONLY the plugins you trust. " + "The permission system is best effort, always " + "assume plugins can bypass permissions and can execute " + "arbitrary code. To see how to create plugins " + + formatRichNamedLink("https://github.com/Chatterino/chatterino2/" + "blob/master/docs/wip-plugins.md", + "look at the manual") + + "."); + scaryLabel->setWordWrap(true); + scaryLabel->setOpenExternalLinks(true); + groupLayout->addRow(scaryLabel); + auto *description = new QLabel("You can load plugins by putting them into " + formatRichNamedLink( From c3b84cb4b6f4b9354195169a78d4d6057373efa5 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 11 May 2024 12:54:27 +0200 Subject: [PATCH 051/105] Add custom image functionality for inline mod buttons. (#5369) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/CMakeLists.txt | 8 +- .../moderationactions/ModerationAction.cpp | 77 ++++++------ .../moderationactions/ModerationAction.hpp | 42 ++++++- .../ModerationActionModel.cpp | 29 ++++- .../ModerationActionModel.hpp | 5 + src/providers/twitch/TwitchBadges.cpp | 49 ++------ src/providers/twitch/TwitchBadges.hpp | 2 +- src/util/LoadPixmap.cpp | 48 ++++++++ src/util/LoadPixmap.hpp | 15 +++ src/widgets/helper/IconDelegate.cpp | 29 +++++ src/widgets/helper/IconDelegate.hpp | 19 +++ src/widgets/settingspages/ModerationPage.cpp | 46 ++++++- tests/CMakeLists.txt | 1 + tests/src/ModerationAction.cpp | 112 ++++++++++++++++++ 15 files changed, 395 insertions(+), 88 deletions(-) create mode 100644 src/util/LoadPixmap.cpp create mode 100644 src/util/LoadPixmap.hpp create mode 100644 src/widgets/helper/IconDelegate.cpp create mode 100644 src/widgets/helper/IconDelegate.hpp create mode 100644 tests/src/ModerationAction.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd5f9a4731..ea438bd6136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Major: Release plugins alpha. (#5288) +- Minor: Add option to customise Moderation buttons with images. (#5369) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 301808906cb..15806aae102 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -500,6 +500,8 @@ set(SOURCE_FILES util/IpcQueue.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp + util/LoadPixmap.cpp + util/LoadPixmap.hpp util/RapidjsonHelpers.cpp util/RapidjsonHelpers.hpp util/RatelimitBucket.cpp @@ -631,6 +633,8 @@ set(SOURCE_FILES widgets/helper/EditableModelView.hpp widgets/helper/EffectLabel.cpp widgets/helper/EffectLabel.hpp + widgets/helper/IconDelegate.cpp + widgets/helper/IconDelegate.hpp widgets/helper/InvisibleSizeGrip.cpp widgets/helper/InvisibleSizeGrip.hpp widgets/helper/NotebookButton.cpp @@ -639,8 +643,6 @@ set(SOURCE_FILES widgets/helper/NotebookTab.hpp widgets/helper/RegExpItemDelegate.cpp widgets/helper/RegExpItemDelegate.hpp - widgets/helper/TrimRegExpValidator.cpp - widgets/helper/TrimRegExpValidator.hpp widgets/helper/ResizingTextEdit.cpp widgets/helper/ResizingTextEdit.hpp widgets/helper/ScrollbarHighlight.cpp @@ -655,6 +657,8 @@ set(SOURCE_FILES widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButtons.cpp widgets/helper/TitlebarButtons.hpp + widgets/helper/TrimRegExpValidator.cpp + widgets/helper/TrimRegExpValidator.hpp widgets/layout/FlowLayout.cpp widgets/layout/FlowLayout.hpp diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index 2b3a95b0642..a82d1848ccb 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -6,28 +6,11 @@ #include "singletons/Resources.hpp" #include +#include namespace chatterino { -// ModerationAction::ModerationAction(Image *_image, const QString &_action) -// : _isImage(true) -// , image(_image) -// , action(_action) -//{ -//} - -// ModerationAction::ModerationAction(const QString &_line1, const QString -// &_line2, -// const QString &_action) -// : _isImage(false) -// , image(nullptr) -// , line1(_line1) -// , line2(_line2) -// , action(_action) -//{ -//} - -ModerationAction::ModerationAction(const QString &action) +ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath) : action_(action) { static QRegularExpression replaceRegex("[!/.]"); @@ -37,6 +20,8 @@ ModerationAction::ModerationAction(const QString &action) if (timeoutMatch.hasMatch()) { + this->type_ = Type::Timeout; + // if (multipleTimeouts > 1) { // QString line1; // QString line2; @@ -99,24 +84,19 @@ ModerationAction::ModerationAction(const QString &action) } this->line2_ = "w"; } - - // line1 = this->line1_; - // line2 = this->line2_; - // } else { - // this->_moderationActions.emplace_back(getResources().buttonTimeout, - // str); - // } } else if (action.startsWith("/ban ")) { - this->imageToLoad_ = 1; + this->type_ = Type::Ban; } else if (action.startsWith("/delete ")) { - this->imageToLoad_ = 2; + this->type_ = Type::Delete; } else { + this->type_ = Type::Custom; + QString xD = action; xD.replace(replaceRegex, ""); @@ -124,6 +104,11 @@ ModerationAction::ModerationAction(const QString &action) this->line1_ = xD.mid(0, 2); this->line2_ = xD.mid(2, 2); } + + if (iconPath.isValid()) + { + this->iconPath_ = iconPath; + } } bool ModerationAction::operator==(const ModerationAction &other) const @@ -139,19 +124,23 @@ bool ModerationAction::isImage() const const std::optional &ModerationAction::getImage() const { assertInGuiThread(); + if (this->image_.has_value()) + { + return this->image_; + } - if (this->imageToLoad_ != 0) + if (this->iconPath_.isValid()) { - if (this->imageToLoad_ == 1) - { - this->image_ = - Image::fromResourcePixmap(getResources().buttons.ban); - } - else if (this->imageToLoad_ == 2) - { - this->image_ = - Image::fromResourcePixmap(getResources().buttons.trashCan); - } + this->image_ = Image::fromUrl({this->iconPath_.toString()}); + } + else if (this->type_ == Type::Ban) + { + this->image_ = Image::fromResourcePixmap(getResources().buttons.ban); + } + else if (this->type_ == Type::Delete) + { + this->image_ = + Image::fromResourcePixmap(getResources().buttons.trashCan); } return this->image_; @@ -172,4 +161,14 @@ const QString &ModerationAction::getAction() const return this->action_; } +const QUrl &ModerationAction::iconPath() const +{ + return this->iconPath_; +} + +ModerationAction::Type ModerationAction::getType() const +{ + return this->type_; +} + } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index 8fa4c9be8a2..643eaf06d62 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -16,7 +17,32 @@ using ImagePtr = std::shared_ptr; class ModerationAction { public: - ModerationAction(const QString &action); + /** + * Type of the action, parsed from the input `action` + */ + enum class Type { + /** + * /ban + */ + Ban, + + /** + * /delete + */ + Delete, + + /** + * /timeout + */ + Timeout, + + /** + * Anything not matching the action types above + */ + Custom, + }; + + ModerationAction(const QString &action, const QUrl &iconPath = {}); bool operator==(const ModerationAction &other) const; @@ -25,13 +51,18 @@ class ModerationAction const QString &getLine1() const; const QString &getLine2() const; const QString &getAction() const; + const QUrl &iconPath() const; + Type getType() const; private: mutable std::optional image_; QString line1_; QString line2_; QString action_; - int imageToLoad_{}; + + Type type_{}; + + QUrl iconPath_; }; } // namespace chatterino @@ -46,6 +77,7 @@ struct Serialize { rapidjson::Value ret(rapidjson::kObjectType); chatterino::rj::set(ret, "pattern", value.getAction(), a); + chatterino::rj::set(ret, "icon", value.iconPath().toString(), a); return ret; } @@ -63,10 +95,12 @@ struct Deserialize { } QString pattern; - chatterino::rj::getSafe(value, "pattern", pattern); - return chatterino::ModerationAction(pattern); + QString icon; + chatterino::rj::getSafe(value, "icon", icon); + + return chatterino::ModerationAction(pattern, QUrl(icon)); } }; diff --git a/src/controllers/moderationactions/ModerationActionModel.cpp b/src/controllers/moderationactions/ModerationActionModel.cpp index d6595556d51..f7160b5896a 100644 --- a/src/controllers/moderationactions/ModerationActionModel.cpp +++ b/src/controllers/moderationactions/ModerationActionModel.cpp @@ -1,13 +1,19 @@ #include "controllers/moderationactions/ModerationActionModel.hpp" #include "controllers/moderationactions/ModerationAction.hpp" +#include "messages/Image.hpp" +#include "util/LoadPixmap.hpp" +#include "util/PostToThread.hpp" #include "util/StandardItemHelper.hpp" +#include +#include + namespace chatterino { // commandmodel ModerationActionModel ::ModerationActionModel(QObject *parent) - : SignalVectorModel(1, parent) + : SignalVectorModel(2, parent) { } @@ -15,14 +21,31 @@ ModerationActionModel ::ModerationActionModel(QObject *parent) ModerationAction ModerationActionModel::getItemFromRow( std::vector &row, const ModerationAction &original) { - return ModerationAction(row[0]->data(Qt::DisplayRole).toString()); + return ModerationAction( + row[Column::Command]->data(Qt::DisplayRole).toString(), + row[Column::Icon]->data(Qt::UserRole).toString()); } // turns a row in the model into a vector item void ModerationActionModel::getRowFromItem(const ModerationAction &item, std::vector &row) { - setStringItem(row[0], item.getAction()); + setStringItem(row[Column::Command], item.getAction()); + setFilePathItem(row[Column::Icon], item.iconPath()); + if (!item.iconPath().isEmpty()) + { + auto oImage = item.getImage(); + assert(oImage.has_value()); + if (oImage.has_value()) + { + auto url = oImage->get()->url(); + loadPixmapFromUrl(url, [row](const QPixmap &pixmap) { + postToThread([row, pixmap]() { + row[Column::Icon]->setData(pixmap, Qt::DecorationRole); + }); + }); + } + } } } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp index e8e51db037c..3382b437803 100644 --- a/src/controllers/moderationactions/ModerationActionModel.hpp +++ b/src/controllers/moderationactions/ModerationActionModel.hpp @@ -13,6 +13,11 @@ class ModerationActionModel : public SignalVectorModel public: explicit ModerationActionModel(QObject *parent); + enum Column { + Command = 0, + Icon = 1, + }; + protected: // turn a vector item into a model row ModerationAction getItemFromRow(std::vector &row, diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index 14c7475e0b9..6e2b4c4aad8 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -7,6 +7,7 @@ #include "messages/Image.hpp" #include "providers/twitch/api/Helix.hpp" #include "util/DisplayBadge.hpp" +#include "util/LoadPixmap.hpp" #include #include @@ -239,48 +240,20 @@ void TwitchBadges::getBadgeIcons(const QList &badges, } } -void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image, +void TwitchBadges::loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback) { - auto url = image->url().string; - NetworkRequest(url) - .concurrent() - .cache() - .onSuccess([this, name, callback, url](auto result) { - auto data = result.getData(); - - // const cast since we are only reading from it - QBuffer buffer(const_cast(&data)); - buffer.open(QIODevice::ReadOnly); - QImageReader reader(&buffer); - - if (!reader.canRead() || reader.size().isEmpty()) - { - qCWarning(chatterinoTwitch) - << "Can't read badge image at" << url << "for" << name - << reader.errorString(); - return; - } - - QImage image = reader.read(); - if (image.isNull()) - { - qCWarning(chatterinoTwitch) - << "Failed reading badge image at" << url << "for" << name - << reader.errorString(); - return; - } + loadPixmapFromUrl(image->url(), + [this, name, callback{std::move(callback)}](auto pixmap) { + auto icon = std::make_shared(pixmap); - auto icon = std::make_shared(QPixmap::fromImage(image)); - - { - std::unique_lock lock(this->badgesMutex_); - this->badgesMap_[name] = icon; - } + { + std::unique_lock lock(this->badgesMutex_); + this->badgesMap_[name] = icon; + } - callback(name, icon); - }) - .execute(); + callback(name, icon); + }); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp index 9964030f079..fff0f5aff0b 100644 --- a/src/providers/twitch/TwitchBadges.hpp +++ b/src/providers/twitch/TwitchBadges.hpp @@ -48,7 +48,7 @@ class TwitchBadges private: void parseTwitchBadges(QJsonObject root); void loaded(); - void loadEmoteImage(const QString &name, ImagePtr image, + void loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback); std::shared_mutex badgesMutex_; diff --git a/src/util/LoadPixmap.cpp b/src/util/LoadPixmap.cpp new file mode 100644 index 00000000000..99fdf95f369 --- /dev/null +++ b/src/util/LoadPixmap.cpp @@ -0,0 +1,48 @@ +#include "util/LoadPixmap.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include +#include + +namespace chatterino { + +void loadPixmapFromUrl(const Url &url, std::function &&callback) +{ + NetworkRequest(url.string) + .concurrent() + .cache() + .onSuccess( + [callback = std::move(callback), url](const NetworkResult &result) { + auto data = result.getData(); + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + QImageReader reader(&buffer); + + if (!reader.canRead() || reader.size().isEmpty()) + { + qCWarning(chatterinoImage) + << "Can't read image file at" << url.string << ":" + << reader.errorString(); + return; + } + + QImage image = reader.read(); + if (image.isNull()) + { + qCWarning(chatterinoImage) + << "Failed reading image at" << url.string << ":" + << reader.errorString(); + return; + } + + callback(QPixmap::fromImage(image)); + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/util/LoadPixmap.hpp b/src/util/LoadPixmap.hpp new file mode 100644 index 00000000000..81fb1192144 --- /dev/null +++ b/src/util/LoadPixmap.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "common/Aliases.hpp" + +#include + +namespace chatterino { + +/** + * Loads an image from url into a QPixmap. Allows for file:// protocol links. Uses cacheing. + * + * @param callback The callback you will get the pixmap by. It will be invoked concurrently with no guarantees on which thread. + */ +void loadPixmapFromUrl(const Url &url, std::function &&callback); + +} // namespace chatterino diff --git a/src/widgets/helper/IconDelegate.cpp b/src/widgets/helper/IconDelegate.cpp new file mode 100644 index 00000000000..c89037eea68 --- /dev/null +++ b/src/widgets/helper/IconDelegate.cpp @@ -0,0 +1,29 @@ +#include "widgets/helper/IconDelegate.hpp" + +#include +#include + +namespace chatterino { + +IconDelegate::IconDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto data = index.data(Qt::DecorationRole); + + if (data.type() != QVariant::Pixmap) + { + return QStyledItemDelegate::paint(painter, option, index); + } + + auto scaledRect = option.rect; + scaledRect.setWidth(scaledRect.height()); + + painter->drawPixmap(scaledRect, data.value()); +} + +} // namespace chatterino diff --git a/src/widgets/helper/IconDelegate.hpp b/src/widgets/helper/IconDelegate.hpp new file mode 100644 index 00000000000..6afd5183ae6 --- /dev/null +++ b/src/widgets/helper/IconDelegate.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace chatterino { + +/** + * IconDelegate draws the decoration role pixmap scaled down to a square icon + */ +class IconDelegate : public QStyledItemDelegate +{ +public: + explicit IconDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/ModerationPage.cpp b/src/widgets/settingspages/ModerationPage.cpp index fce69eff00e..65ba577b1ae 100644 --- a/src/widgets/settingspages/ModerationPage.cpp +++ b/src/widgets/settingspages/ModerationPage.cpp @@ -9,12 +9,16 @@ #include "singletons/Settings.hpp" #include "util/Helpers.hpp" #include "util/LayoutCreator.hpp" +#include "util/LoadPixmap.hpp" +#include "util/PostToThread.hpp" #include "widgets/helper/EditableModelView.hpp" +#include "widgets/helper/IconDelegate.hpp" #include #include #include #include +#include #include #include #include @@ -207,11 +211,51 @@ ModerationPage::ModerationPage() ->initialized(&getSettings()->moderationActions)) .getElement(); - view->setTitles({"Actions"}); + view->setTitles({"Action", "Icon"}); view->getTableView()->horizontalHeader()->setSectionResizeMode( QHeaderView::Fixed); view->getTableView()->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); + view->getTableView()->setItemDelegateForColumn( + ModerationActionModel::Column::Icon, new IconDelegate(view)); + QObject::connect( + view->getTableView(), &QTableView::clicked, + [this, view](const QModelIndex &clicked) { + if (clicked.column() == ModerationActionModel::Column::Icon) + { + auto fileUrl = QFileDialog::getOpenFileUrl( + this, "Open Image", QUrl(), + "Image Files (*.png *.jpg *.jpeg)"); + view->getModel()->setData(clicked, fileUrl, Qt::UserRole); + view->getModel()->setData(clicked, fileUrl.fileName(), + Qt::DisplayRole); + // Clear the icon if the user canceled the dialog + if (fileUrl.isEmpty()) + { + view->getModel()->setData(clicked, QVariant(), + Qt::DecorationRole); + } + else + { + // QPointer will be cleared when view is destroyed + QPointer viewtemp = view; + + loadPixmapFromUrl( + {fileUrl.toString()}, + [clicked, view = viewtemp](const QPixmap &pixmap) { + postToThread([clicked, view, pixmap]() { + if (view.isNull()) + { + return; + } + + view->getModel()->setData( + clicked, pixmap, Qt::DecorationRole); + }); + }); + } + } + }); // We can safely ignore this signal connection since we own the view std::ignore = view->addButtonPressed.connect([] { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bd35b79dee9..8ea086b13c7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp # Add your new file above this line! ) diff --git a/tests/src/ModerationAction.cpp b/tests/src/ModerationAction.cpp new file mode 100644 index 00000000000..ce32fb39d02 --- /dev/null +++ b/tests/src/ModerationAction.cpp @@ -0,0 +1,112 @@ +#include "controllers/moderationactions/ModerationAction.hpp" + +#include "messages/Image.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" + +#include +#include + +using namespace chatterino; + +using namespace std::chrono_literals; + +namespace { + +class MockApplication : mock::EmptyApplication +{ +public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + { + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + Settings settings; + Emotes emotes; +}; + +class ModerationActionTest : public ::testing::Test +{ +public: + MockApplication mockApplication; +}; + +} // namespace + +TEST_F(ModerationActionTest, Parse) +{ + struct TestCase { + QString action; + QString iconPath; + + QString expectedLine1; + QString expectedLine2; + + std::optional expectedImage; + + ModerationAction::Type expectedType; + }; + + std::vector tests{ + { + .action = "/ban forsen", + .expectedImage = + Image::fromResourcePixmap(getResources().buttons.ban), + .expectedType = ModerationAction::Type::Ban, + }, + { + .action = "/delete {message.id}", + .expectedImage = + Image::fromResourcePixmap(getResources().buttons.trashCan), + .expectedType = ModerationAction::Type::Delete, + }, + { + .action = "/timeout {user.name} 1d", + .expectedLine1 = "1", + .expectedLine2 = "d", + .expectedType = ModerationAction::Type::Timeout, + }, + { + .action = ".timeout {user.name} 300", + .expectedLine1 = "5", + .expectedLine2 = "m", + .expectedType = ModerationAction::Type::Timeout, + }, + { + .action = "forsen", + .expectedLine1 = "fo", + .expectedLine2 = "rs", + .expectedType = ModerationAction::Type::Custom, + }, + { + .action = "forsen", + .iconPath = "file:///this-is-the-path-to-the-icon.png", + .expectedLine1 = "fo", + .expectedLine2 = "rs", + .expectedImage = + Image::fromUrl(Url{"file:///this-is-the-path-to-the-icon.png"}), + .expectedType = ModerationAction::Type::Custom, + }, + }; + + for (const auto &test : tests) + { + ModerationAction moderationAction(test.action, test.iconPath); + + EXPECT_EQ(moderationAction.getAction(), test.action); + + EXPECT_EQ(moderationAction.getLine1(), test.expectedLine1); + EXPECT_EQ(moderationAction.getLine2(), test.expectedLine2); + + EXPECT_EQ(moderationAction.getImage(), test.expectedImage); + + EXPECT_EQ(moderationAction.getType(), test.expectedType); + } +} From 5c539ebe9a735757541d72b5d06a50d810baea6e Mon Sep 17 00:00:00 2001 From: Brian <18603393+brian6932@users.noreply.github.com> Date: Sat, 11 May 2024 12:52:25 -0400 Subject: [PATCH 052/105] fix: Missing includes when building with `USE_PRECOMPILED_HEADERS=OFF` (#5389) --- src/util/AttachToConsole.cpp | 1 + src/widgets/helper/TitlebarButtons.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/util/AttachToConsole.cpp b/src/util/AttachToConsole.cpp index 41689c699af..5f887260e21 100644 --- a/src/util/AttachToConsole.cpp +++ b/src/util/AttachToConsole.cpp @@ -3,6 +3,7 @@ #ifdef USEWINSDK # include +# include # include #endif diff --git a/src/widgets/helper/TitlebarButtons.hpp b/src/widgets/helper/TitlebarButtons.hpp index 42a430d6942..e7ee3eb5bf5 100644 --- a/src/widgets/helper/TitlebarButtons.hpp +++ b/src/widgets/helper/TitlebarButtons.hpp @@ -3,6 +3,7 @@ class QPoint; class QWidget; +#include #include namespace chatterino { From 8202cd0d9969fb88becb8aeaac607fc56697b541 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 12:52:58 +0200 Subject: [PATCH 053/105] refactor: cleanup and document `Scrollbar` (#5334) Co-authored-by: Rasmus Karlsson Co-authored-by: Daniel Sage --- CHANGELOG.md | 1 + src/widgets/Scrollbar.cpp | 323 ++++++++++------------- src/widgets/Scrollbar.hpp | 138 ++++++++-- src/widgets/dialogs/EmotePopup.cpp | 4 +- src/widgets/dialogs/ReplyThreadPopup.cpp | 4 +- src/widgets/dialogs/UserInfoPopup.cpp | 4 +- src/widgets/helper/ChannelView.cpp | 6 +- src/widgets/splits/Split.cpp | 4 +- tests/CMakeLists.txt | 1 + tests/src/ModerationAction.cpp | 2 +- tests/src/Scrollbar.cpp | 187 +++++++++++++ 11 files changed, 451 insertions(+), 223 deletions(-) create mode 100644 tests/src/Scrollbar.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ea438bd6136..631b3eb1d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) +- Dev: Refactor and document `Scrollbar`. (#5334) ## 2.5.1 diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 6ca0e248e96..e1492b67321 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -4,7 +4,6 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" -#include "util/Clamp.hpp" #include "widgets/helper/ChannelView.hpp" #include @@ -13,7 +12,19 @@ #include -#define MIN_THUMB_HEIGHT 10 +namespace { + +constexpr int MIN_THUMB_HEIGHT = 10; + +/// Amount of messages to move by when clicking on the track +constexpr qreal SCROLL_DELTA = 5.0; + +bool areClose(auto a, auto b) +{ + return std::abs(a - b) <= 0.0001; +} + +} // namespace namespace chatterino { @@ -22,40 +33,51 @@ Scrollbar::Scrollbar(size_t messagesLimit, ChannelView *parent) , currentValueAnimation_(this, "currentValue_") , highlights_(messagesLimit) { - this->resize(int(16 * this->scale()), 100); + this->resize(static_cast(16 * this->scale()), 100); this->currentValueAnimation_.setDuration(150); this->currentValueAnimation_.setEasingCurve( QEasingCurve(QEasingCurve::OutCubic)); connect(&this->currentValueAnimation_, &QAbstractAnimation::finished, this, - &Scrollbar::resetMaximum); + &Scrollbar::resetBounds); this->setMouseTracking(true); } -void Scrollbar::addHighlight(ScrollbarHighlight highlight) +boost::circular_buffer Scrollbar::getHighlights() const { - this->highlights_.pushBack(highlight); + return this->highlights_; } -void Scrollbar::addHighlightsAtStart( - const std::vector &_highlights) +void Scrollbar::addHighlight(ScrollbarHighlight highlight) { - this->highlights_.pushFront(_highlights); + this->highlights_.push_back(std::move(highlight)); } -void Scrollbar::replaceHighlight(size_t index, ScrollbarHighlight replacement) +void Scrollbar::addHighlightsAtStart( + const std::vector &highlights) { - this->highlights_.replaceItem(index, replacement); -} + size_t nItems = std::min(highlights.size(), this->highlights_.capacity() - + this->highlights_.size()); -void Scrollbar::pauseHighlights() -{ - this->highlightsPaused_ = true; + if (nItems == 0) + { + return; + } + + for (size_t i = 0; i < nItems; i++) + { + this->highlights_.push_front(highlights[highlights.size() - 1 - i]); + } } -void Scrollbar::unpauseHighlights() +void Scrollbar::replaceHighlight(size_t index, ScrollbarHighlight replacement) { - this->highlightsPaused_ = false; + if (this->highlights_.size() <= index) + { + return; + } + + this->highlights_[index] = std::move(replacement); } void Scrollbar::clearHighlights() @@ -63,16 +85,6 @@ void Scrollbar::clearHighlights() this->highlights_.clear(); } -LimitedQueueSnapshot &Scrollbar::getHighlightSnapshot() -{ - if (!this->highlightsPaused_) - { - this->highlightSnapshot_ = this->highlights_.getSnapshot(); - } - - return this->highlightSnapshot_; -} - void Scrollbar::scrollToBottom(bool animate) { this->setDesiredValue(this->getBottom(), animate); @@ -102,7 +114,7 @@ void Scrollbar::offsetMaximum(qreal value) this->updateScroll(); } -void Scrollbar::resetMaximum() +void Scrollbar::resetBounds() { if (this->minimum_ > 0) { @@ -132,26 +144,19 @@ void Scrollbar::offsetMinimum(qreal value) this->updateScroll(); } -void Scrollbar::setLargeChange(qreal value) +void Scrollbar::setPageSize(qreal value) { - this->largeChange_ = value; - - this->updateScroll(); -} - -void Scrollbar::setSmallChange(qreal value) -{ - this->smallChange_ = value; + this->pageSize_ = value; this->updateScroll(); } void Scrollbar::setDesiredValue(qreal value, bool animated) { - value = std::max(this->minimum_, std::min(this->getBottom(), value)); - - if (std::abs(this->currentValue_ - value) <= 0.0001) + value = std::clamp(value, this->minimum_, this->getBottom()); + if (areClose(this->currentValue_, value)) { + // value has not changed return; } @@ -159,7 +164,7 @@ void Scrollbar::setDesiredValue(qreal value, bool animated) this->desiredValueChanged_.invoke(); - this->atBottom_ = (this->getBottom() - value) <= 0.0001; + this->atBottom_ = areClose(this->getBottom(), value); if (animated && getSettings()->enableSmoothScrolling) { @@ -178,7 +183,7 @@ void Scrollbar::setDesiredValue(qreal value, bool animated) else { this->setCurrentValue(value); - this->resetMaximum(); + this->resetBounds(); } } } @@ -193,19 +198,14 @@ qreal Scrollbar::getMinimum() const return this->minimum_; } -qreal Scrollbar::getLargeChange() const +qreal Scrollbar::getPageSize() const { - return this->largeChange_; + return this->pageSize_; } qreal Scrollbar::getBottom() const { - return this->maximum_ - this->largeChange_; -} - -qreal Scrollbar::getSmallChange() const -{ - return this->smallChange_; + return this->maximum_ - this->pageSize_; } qreal Scrollbar::getDesiredValue() const @@ -222,8 +222,8 @@ qreal Scrollbar::getRelativeCurrentValue() const { // currentValue - minimum can be negative if minimum is incremented while // scrolling up to or down from the top when smooth scrolling is enabled. - return clamp(this->currentValue_ - this->minimum_, qreal(0.0), - this->currentValue_); + return std::clamp(this->currentValue_ - this->minimum_, 0.0, + this->currentValue_); } void Scrollbar::offset(qreal value) @@ -244,9 +244,9 @@ pajlada::Signals::NoArgSignal &Scrollbar::getDesiredValueChanged() void Scrollbar::setCurrentValue(qreal value) { value = std::max(this->minimum_, std::min(this->getBottom(), value)); - - if (std::abs(this->currentValue_ - value) <= 0.0001) + if (areClose(this->currentValue_, value)) { + // value has not changed return; } @@ -258,21 +258,24 @@ void Scrollbar::setCurrentValue(qreal value) void Scrollbar::printCurrentState(const QString &prefix) const { - qCDebug(chatterinoWidget) - << prefix // - << "Current value: " << this->getCurrentValue() // - << ". Maximum: " << this->getMaximum() // - << ". Minimum: " << this->getMinimum() // - << ". Large change: " << this->getLargeChange(); // + qCDebug(chatterinoWidget).nospace().noquote() + << prefix // + << " { currentValue: " << this->getCurrentValue() // + << ", desiredValue: " << this->getDesiredValue() // + << ", maximum: " << this->getMaximum() // + << ", minimum: " << this->getMinimum() // + << ", pageSize: " << this->getPageSize() // + << " }"; } -void Scrollbar::paintEvent(QPaintEvent *) +void Scrollbar::paintEvent(QPaintEvent * /*event*/) { - bool mouseOver = this->mouseOverIndex_ != -1; - int xOffset = mouseOver ? 0 : width() - int(4 * this->scale()); + bool mouseOver = this->mouseOverLocation_ != MouseLocation::Outside; + int xOffset = + mouseOver ? 0 : this->width() - static_cast(4.0F * this->scale()); QPainter painter(this); - painter.fillRect(rect(), this->theme->scrollbars.background); + painter.fillRect(this->rect(), this->theme->scrollbars.background); bool enableRedeemedHighlights = getSettings()->enableRedeemedHighlight; bool enableFirstMessageHighlights = @@ -280,16 +283,10 @@ void Scrollbar::paintEvent(QPaintEvent *) bool enableElevatedMessageHighlights = getSettings()->enableElevatedMessageHighlight; - // painter.fillRect(QRect(xOffset, 0, width(), this->buttonHeight), - // this->themeManager->ScrollbarArrow); - // painter.fillRect(QRect(xOffset, height() - this->buttonHeight, - // width(), this->buttonHeight), - // this->themeManager->ScrollbarArrow); - this->thumbRect_.setX(xOffset); // mouse over thumb - if (this->mouseDownIndex_ == 2) + if (this->mouseDownLocation_ == MouseLocation::InsideThumb) { painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumbSelected); @@ -301,23 +298,21 @@ void Scrollbar::paintEvent(QPaintEvent *) } // draw highlights - auto &snapshot = this->getHighlightSnapshot(); - size_t snapshotLength = snapshot.size(); - - if (snapshotLength == 0) + if (this->highlights_.empty()) { return; } + size_t nHighlights = this->highlights_.size(); int w = this->width(); - float y = 0; - float dY = float(this->height()) / float(snapshotLength); + float dY = + static_cast(this->height()) / static_cast(nHighlights); int highlightHeight = - int(std::ceil(std::max(this->scale() * 2, dY))); + static_cast(std::ceil(std::max(this->scale() * 2.0F, dY))); - for (size_t i = 0; i < snapshotLength; i++, y += dY) + for (size_t i = 0; i < nHighlights; i++) { - ScrollbarHighlight const &highlight = snapshot[i]; + const auto &highlight = this->highlights_[i]; if (highlight.isNull()) { @@ -344,16 +339,16 @@ void Scrollbar::paintEvent(QPaintEvent *) QColor color = highlight.getColor(); color.setAlpha(255); + int y = static_cast(dY * static_cast(i)); switch (highlight.getStyle()) { case ScrollbarHighlight::Default: { - painter.fillRect(w / 8 * 3, int(y), w / 4, highlightHeight, - color); + painter.fillRect(w / 8 * 3, y, w / 4, highlightHeight, color); } break; case ScrollbarHighlight::Line: { - painter.fillRect(0, int(y), w, 1, color); + painter.fillRect(0, y, w, 1, color); } break; @@ -362,52 +357,30 @@ void Scrollbar::paintEvent(QPaintEvent *) } } -void Scrollbar::resizeEvent(QResizeEvent *) +void Scrollbar::resizeEvent(QResizeEvent * /*event*/) { - this->resize(int(16 * this->scale()), this->height()); + this->resize(static_cast(16 * this->scale()), this->height()); } void Scrollbar::mouseMoveEvent(QMouseEvent *event) { - if (this->mouseDownIndex_ == -1) + if (this->mouseDownLocation_ == MouseLocation::Outside) { - int y = event->pos().y(); - - auto oldIndex = this->mouseOverIndex_; - - if (y < this->buttonHeight_) - { - this->mouseOverIndex_ = 0; - } - else if (y < this->thumbRect_.y()) - { - this->mouseOverIndex_ = 1; - } - else if (this->thumbRect_.contains(2, y)) - { - this->mouseOverIndex_ = 2; - } - else if (y < height() - this->buttonHeight_) - { - this->mouseOverIndex_ = 3; - } - else - { - this->mouseOverIndex_ = 4; - } - - if (oldIndex != this->mouseOverIndex_) + auto moveLocation = this->locationOfMouseEvent(event); + if (this->mouseOverLocation_ != moveLocation) { + this->mouseOverLocation_ = moveLocation; this->update(); } } - else if (this->mouseDownIndex_ == 2) + else if (this->mouseDownLocation_ == MouseLocation::InsideThumb) { - int delta = event->pos().y() - this->lastMousePosition_.y(); + qreal delta = + static_cast(event->pos().y() - this->lastMousePosition_.y()); this->setDesiredValue( this->desiredValue_ + - (qreal(delta) / std::max(0.00000002, this->trackHeight_)) * + (delta / std::max(0.00000002, this->trackHeight_)) * this->maximum_); } @@ -416,98 +389,80 @@ void Scrollbar::mouseMoveEvent(QMouseEvent *event) void Scrollbar::mousePressEvent(QMouseEvent *event) { - int y = event->pos().y(); - - if (y < this->buttonHeight_) - { - this->mouseDownIndex_ = 0; - } - else if (y < this->thumbRect_.y()) - { - this->mouseDownIndex_ = 1; - } - else if (this->thumbRect_.contains(2, y)) - { - this->mouseDownIndex_ = 2; - } - else if (y < height() - this->buttonHeight_) - { - this->mouseDownIndex_ = 3; - } - else - { - this->mouseDownIndex_ = 4; - } + this->mouseDownLocation_ = this->locationOfMouseEvent(event); + this->update(); } void Scrollbar::mouseReleaseEvent(QMouseEvent *event) { - int y = event->pos().y(); - - if (y < this->buttonHeight_) + auto releaseLocation = this->locationOfMouseEvent(event); + if (this->mouseDownLocation_ != releaseLocation) { - if (this->mouseDownIndex_ == 0) - { - this->setDesiredValue(this->desiredValue_ - this->smallChange_, - true); - } - } - else if (y < this->thumbRect_.y()) - { - if (this->mouseDownIndex_ == 1) - { - this->setDesiredValue(this->desiredValue_ - this->smallChange_, - true); - } - } - else if (this->thumbRect_.contains(2, y)) - { - // do nothing - } - else if (y < height() - this->buttonHeight_) - { - if (this->mouseDownIndex_ == 3) - { - this->setDesiredValue(this->desiredValue_ + this->smallChange_, - true); - } + // Ignore event. User released the mouse from a different spot than + // they first clicked. For example, they clicked above the thumb, + // changed their mind, dragged the mouse below the thumb, and released. + this->mouseDownLocation_ = MouseLocation::Outside; + return; } - else + + switch (releaseLocation) { - if (this->mouseDownIndex_ == 4) - { - this->setDesiredValue(this->desiredValue_ + this->smallChange_, - true); - } + case MouseLocation::AboveThumb: + // Move scrollbar up a small bit. + this->setDesiredValue(this->desiredValue_ - SCROLL_DELTA, true); + break; + case MouseLocation::BelowThumb: + // Move scrollbar down a small bit. + this->setDesiredValue(this->desiredValue_ + SCROLL_DELTA, true); + break; + default: + break; } - this->mouseDownIndex_ = -1; - + this->mouseDownLocation_ = MouseLocation::Outside; this->update(); } -void Scrollbar::leaveEvent(QEvent *) +void Scrollbar::leaveEvent(QEvent * /*event*/) { - this->mouseOverIndex_ = -1; - + this->mouseOverLocation_ = MouseLocation::Outside; this->update(); } void Scrollbar::updateScroll() { - this->trackHeight_ = this->height() - this->buttonHeight_ - - this->buttonHeight_ - MIN_THUMB_HEIGHT - 1; + this->trackHeight_ = this->height() - MIN_THUMB_HEIGHT - 1; auto div = std::max(0.0000001, this->maximum_ - this->minimum_); - this->thumbRect_ = QRect( - 0, - int((this->getRelativeCurrentValue()) / div * this->trackHeight_) + 1 + - this->buttonHeight_, - this->width(), - int(this->largeChange_ / div * this->trackHeight_) + MIN_THUMB_HEIGHT); + this->thumbRect_ = + QRect(0, + static_cast((this->getRelativeCurrentValue()) / div * + this->trackHeight_) + + 1, + this->width(), + static_cast(this->pageSize_ / div * this->trackHeight_) + + MIN_THUMB_HEIGHT); this->update(); } +Scrollbar::MouseLocation Scrollbar::locationOfMouseEvent( + QMouseEvent *event) const +{ + int y = event->pos().y(); + + if (y < this->thumbRect_.y()) + { + return MouseLocation::AboveThumb; + } + + if (this->thumbRect_.contains(2, y)) + { + return MouseLocation::InsideThumb; + } + + return MouseLocation::BelowThumb; +} + } // namespace chatterino diff --git a/src/widgets/Scrollbar.hpp b/src/widgets/Scrollbar.hpp index d025539c12a..08a843586fa 100644 --- a/src/widgets/Scrollbar.hpp +++ b/src/widgets/Scrollbar.hpp @@ -1,11 +1,10 @@ #pragma once -#include "messages/LimitedQueue.hpp" #include "widgets/BaseWidget.hpp" #include "widgets/helper/ScrollbarHighlight.hpp" +#include #include -#include #include #include @@ -13,41 +12,119 @@ namespace chatterino { class ChannelView; +/// @brief A scrollbar for views with partially laid out items +/// +/// This scrollbar is made for views that only lay out visible items. This is +/// the case for a @a ChannelView for example. There, only the visible messages +/// are laid out. For a traditional scrollbar, all messages would need to be +/// laid out to be able to compute the total height of all items. However, for +/// these messages this isn't possible. +/// +/// To avoid having to lay out all items, this scrollbar tracks the position of +/// the content in messages (as opposed to pixels). The position is given by +/// `currentValue` which refers to the index of the message at the top plus a +/// fraction inside the message. The position can be animated to have a smooth +/// scrolling effect. In this case, `currentValue` refers to the displayed +/// position and `desiredValue` refers to the position the scrollbar is set to +/// be at after the animation. The latter is used for example to check if the +/// scrollbar is at the bottom. +/// +/// `minimum` and `maximum` are used to map scrollbar positions to +/// (message-)buffer indices. The buffer is of size `maximum - minimum` and an +/// index is computed by `scrollbarPos - minimum` - thus a scrollbar position +/// of a message is at `index + minimum. +/// +/// @cond src-only +/// +/// The following illustrates a scrollbar in a channel view with seven +/// messages. The scrollbar is at the bottom. No animation is active, thus +/// `currentValue = desiredValue`. +/// +/// ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐←╌╌╌ minimum +/// Alice: This message is quite = 0 +/// ┬ ╭─────────────────────────────────╮←╮ +/// │ │ long, so it gets wrapped │ ┆ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ ╰╌╌╌ currentValue +/// │ │ Bob: are you sure? │ = 0.5 +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ = desiredValue +/// pageSize ╌╌╌┤ │ Alice: Works for me... try for │ = maximum +/// = 6.5 │ │ yourself │ - pageSize +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ = bottom +/// │ │ Bob: I'm trying to get my really│ ⇒ atBottom = true +/// │ │ long message to wrap so I can │ +/// │ │ debug this issue I'm facing... │ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +/// │ │ Bob: Omg it worked │ +/// │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +/// │ │ Alice: That's amazing! ╭┤ ┬ +/// │ │╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌││ ├╌╌ thumbRect.height() +/// │ │ Bob: you're right ╰┤ ┴ +/// ┴╭→╰─────────────────────────────────╯ +/// ┆ +/// maximum +/// = 7 +/// @endcond +/// +/// When messages are added at the bottom, both maximum and minimum are offset +/// by 1 and after a layout, the desired value is updated, causing the content +/// to move. Afterwards, the bounds are reset (potentially waiting for the +/// animation to finish). +/// +/// While scrolling is paused, the desired (and current) value won't be +/// updated. However, messages can still come in and "shift" the values in the +/// backing ring-buffer. If the current value would be used, the messages would +/// still shift upwards (just at a different offset). To avoid this, there's a +/// _relative current value_, which is `currentValue - minimum`. It's the +/// actual index of the top message in the buffer. Since the minimum is shifted +/// by 1 when messages come in, the view will remain idle (visually). class Scrollbar : public BaseWidget { Q_OBJECT public: - Scrollbar(size_t messagesLimit, ChannelView *parent = nullptr); + Scrollbar(size_t messagesLimit, ChannelView *parent); + /// Return a copy of the highlights + /// + /// Should only be used for tests + boost::circular_buffer getHighlights() const; void addHighlight(ScrollbarHighlight highlight); void addHighlightsAtStart( const std::vector &highlights_); void replaceHighlight(size_t index, ScrollbarHighlight replacement); - void pauseHighlights(); - void unpauseHighlights(); void clearHighlights(); void scrollToBottom(bool animate = false); void scrollToTop(bool animate = false); bool isAtBottom() const; + qreal getMaximum() const; void setMaximum(qreal value); void offsetMaximum(qreal value); - void resetMaximum(); + + qreal getMinimum() const; void setMinimum(qreal value); void offsetMinimum(qreal value); - void setLargeChange(qreal value); - void setSmallChange(qreal value); + + void resetBounds(); + + qreal getPageSize() const; + void setPageSize(qreal value); + + qreal getDesiredValue() const; void setDesiredValue(qreal value, bool animated = false); - qreal getMaximum() const; - qreal getMinimum() const; - qreal getLargeChange() const; + + /// The bottom-most scroll position qreal getBottom() const; - qreal getSmallChange() const; - qreal getDesiredValue() const; qreal getCurrentValue() const; + + /// @brief The current value relative to the minimum + /// + /// > currentValue - minimum + /// + /// This should be used as an index into a buffer of messages, as it is + /// unaffected by simultaneous shifts of minimum and maximum. qreal getRelativeCurrentValue() const; // offset the desired value without breaking smooth scolling @@ -56,47 +133,54 @@ class Scrollbar : public BaseWidget pajlada::Signals::NoArgSignal &getDesiredValueChanged(); void setCurrentValue(qreal value); - void printCurrentState(const QString &prefix = QString()) const; + void printCurrentState( + const QString &prefix = QStringLiteral("Scrollbar")) const; Q_PROPERTY(qreal desiredValue_ READ getDesiredValue WRITE setDesiredValue) protected: - void paintEvent(QPaintEvent *) override; - void resizeEvent(QResizeEvent *) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - void leaveEvent(QEvent *) override; + void leaveEvent(QEvent *event) override; private: Q_PROPERTY(qreal currentValue_ READ getCurrentValue WRITE setCurrentValue) - LimitedQueueSnapshot &getHighlightSnapshot(); void updateScroll(); - QMutex mutex_; + enum class MouseLocation { + /// The mouse is positioned outside the scrollbar + Outside, + /// The mouse is positioned inside the scrollbar, but above the thumb (the thing you can drag inside the scrollbar) + AboveThumb, + /// The mouse is positioned inside the scrollbar, and on top of the thumb + InsideThumb, + /// The mouse is positioned inside the scrollbar, but below the thumb + BelowThumb, + }; + + MouseLocation locationOfMouseEvent(QMouseEvent *event) const; QPropertyAnimation currentValueAnimation_; - LimitedQueue highlights_; - bool highlightsPaused_{false}; - LimitedQueueSnapshot highlightSnapshot_; + boost::circular_buffer highlights_; bool atBottom_{false}; - int mouseOverIndex_ = -1; - int mouseDownIndex_ = -1; + MouseLocation mouseOverLocation_ = MouseLocation::Outside; + MouseLocation mouseDownLocation_ = MouseLocation::Outside; QPoint lastMousePosition_; - int buttonHeight_ = 0; int trackHeight_ = 100; QRect thumbRect_; qreal maximum_ = 0; qreal minimum_ = 0; - qreal largeChange_ = 0; - qreal smallChange_ = 5; + qreal pageSize_ = 0; qreal desiredValue_ = 0; qreal currentValue_ = 0; diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 71a18a0a3c3..fd80d7e956b 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -350,11 +350,11 @@ void EmotePopup::addShortcuts() auto &scrollbar = channelView->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index 32ee5a52920..4d3dd3a83b3 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -51,11 +51,11 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, Split *split) auto &scrollbar = this->ui_.threadView->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 953be229cde..fff4fefc15c 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -164,11 +164,11 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split) auto &scrollbar = this->ui_.latestMessages->getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index c5a679727fe..819b7d0c0bf 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -744,7 +744,7 @@ void ChannelView::updateScrollbar( if (h < 0) // break condition { - this->scrollBar_->setLargeChange( + this->scrollBar_->setPageSize( (messages.size() - i) + qreal(h) / std::max(1, message->getHeight())); @@ -778,7 +778,7 @@ void ChannelView::clearMessages() // Clear all stored messages in this chat widget this->messages_.clear(); this->scrollBar_->clearHighlights(); - this->scrollBar_->resetMaximum(); + this->scrollBar_->resetBounds(); this->scrollBar_->setMaximum(0); this->scrollBar_->setMinimum(0); this->queueLayout(); @@ -1277,7 +1277,7 @@ void ChannelView::messagesUpdated() this->messages_.clear(); this->scrollBar_->clearHighlights(); - this->scrollBar_->resetMaximum(); + this->scrollBar_->resetBounds(); this->scrollBar_->setMaximum(qreal(snapshot.size())); this->scrollBar_->setMinimum(0); this->lastMessageHasAlternateBackground_ = false; diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 81b0e2f65cf..1e34aefbf82 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -553,11 +553,11 @@ void Split::addShortcuts() auto &scrollbar = this->getChannelView().getScrollBar(); if (direction == "up") { - scrollbar.offset(-scrollbar.getLargeChange()); + scrollbar.offset(-scrollbar.getPageSize()); } else if (direction == "down") { - scrollbar.offset(scrollbar.getLargeChange()); + scrollbar.offset(scrollbar.getPageSize()); } else { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ea086b13c7..8288664dfd8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -43,6 +43,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/MessageLayout.cpp ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp # Add your new file above this line! ) diff --git a/tests/src/ModerationAction.cpp b/tests/src/ModerationAction.cpp index ce32fb39d02..75daf8e3e22 100644 --- a/tests/src/ModerationAction.cpp +++ b/tests/src/ModerationAction.cpp @@ -5,8 +5,8 @@ #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "Test.hpp" -#include #include using namespace chatterino; diff --git a/tests/src/Scrollbar.cpp b/tests/src/Scrollbar.cpp new file mode 100644 index 00000000000..98ca9a64062 --- /dev/null +++ b/tests/src/Scrollbar.cpp @@ -0,0 +1,187 @@ +#include "widgets/Scrollbar.hpp" + +#include "Application.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" +#include "Test.hpp" +#include "widgets/helper/ScrollbarHighlight.hpp" + +#include + +#include + +using namespace chatterino; + +namespace { + +class MockApplication : mock::EmptyApplication +{ +public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + , fonts(this->settings) + , windowManager(this->paths_) + { + } + Theme *getThemes() override + { + return &this->theme; + } + + Fonts *getFonts() override + { + return &this->fonts; + } + + WindowManager *getWindows() override + { + return &this->windowManager; + } + + Settings settings; + Theme theme; + Fonts fonts; + WindowManager windowManager; +}; + +} // namespace + +TEST(Scrollbar, AddHighlight) +{ + MockApplication mockApplication; + + Scrollbar scrollbar(10, nullptr); + EXPECT_EQ(scrollbar.getHighlights().size(), 0); + + for (int i = 0; i < 15; ++i) + { + auto color = std::make_shared(i, 0, 0); + ScrollbarHighlight scrollbarHighlight{color}; + scrollbar.addHighlight(scrollbarHighlight); + } + + EXPECT_EQ(scrollbar.getHighlights().size(), 10); + auto highlights = scrollbar.getHighlights(); + for (int i = 0; i < 10; ++i) + { + auto highlight = highlights[i]; + EXPECT_EQ(highlight.getColor().red(), i + 5); + } +} + +TEST(Scrollbar, AddHighlightsAtStart) +{ + MockApplication mockApplication; + + Scrollbar scrollbar(10, nullptr); + EXPECT_EQ(scrollbar.getHighlights().size(), 0); + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(1, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 1); + EXPECT_EQ(highlights[0].getColor().red(), 1); + } + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(2, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 2); + EXPECT_EQ(highlights[0].getColor().red(), 2); + EXPECT_EQ(highlights[1].getColor().red(), 1); + } + + { + scrollbar.addHighlightsAtStart({ + { + std::make_shared(4, 0, 0), + }, + { + std::make_shared(3, 0, 0), + }, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 4); + EXPECT_EQ(highlights[0].getColor().red(), 4); + EXPECT_EQ(highlights[1].getColor().red(), 3); + EXPECT_EQ(highlights[2].getColor().red(), 2); + EXPECT_EQ(highlights[3].getColor().red(), 1); + } + + { + // Adds as many as it can, in reverse order + scrollbar.addHighlightsAtStart({ + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(10, 0, 0)}, + {std::make_shared(9, 0, 0)}, + {std::make_shared(8, 0, 0)}, + {std::make_shared(7, 0, 0)}, + {std::make_shared(6, 0, 0)}, + {std::make_shared(5, 0, 0)}, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 10); + for (const auto &highlight : highlights) + { + std::cout << highlight.getColor().red() << '\n'; + } + EXPECT_EQ(highlights[0].getColor().red(), 10); + EXPECT_EQ(highlights[1].getColor().red(), 9); + EXPECT_EQ(highlights[2].getColor().red(), 8); + EXPECT_EQ(highlights[3].getColor().red(), 7); + EXPECT_EQ(highlights[4].getColor().red(), 6); + EXPECT_EQ(highlights[5].getColor().red(), 5); + EXPECT_EQ(highlights[6].getColor().red(), 4); + EXPECT_EQ(highlights[7].getColor().red(), 3); + EXPECT_EQ(highlights[8].getColor().red(), 2); + EXPECT_EQ(highlights[9].getColor().red(), 1); + } + + { + // Adds as many as it can, in reverse order + // Since the highlights are already full, nothing will be added + scrollbar.addHighlightsAtStart({ + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + {std::make_shared(255, 0, 0)}, + }); + auto highlights = scrollbar.getHighlights(); + EXPECT_EQ(highlights.size(), 10); + for (const auto &highlight : highlights) + { + std::cout << highlight.getColor().red() << '\n'; + } + EXPECT_EQ(highlights[0].getColor().red(), 10); + EXPECT_EQ(highlights[1].getColor().red(), 9); + EXPECT_EQ(highlights[2].getColor().red(), 8); + EXPECT_EQ(highlights[3].getColor().red(), 7); + EXPECT_EQ(highlights[4].getColor().red(), 6); + EXPECT_EQ(highlights[5].getColor().red(), 5); + EXPECT_EQ(highlights[6].getColor().red(), 4); + EXPECT_EQ(highlights[7].getColor().red(), 3); + EXPECT_EQ(highlights[8].getColor().red(), 2); + EXPECT_EQ(highlights[9].getColor().red(), 1); + } +} From febcf464fe9f9caa78fbfb0d3fb2db8cb372ee22 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 13:59:14 +0200 Subject: [PATCH 054/105] Use Qt's High-DPI scaling on Windows (#4868) --- CHANGELOG.md | 2 + resources/qss/settings.qss | 6 +- src/RunGui.cpp | 4 - src/main.cpp | 5 - src/messages/MessageElement.cpp | 10 +- src/messages/layouts/MessageLayout.cpp | 13 +- src/messages/layouts/MessageLayout.hpp | 5 +- .../layouts/MessageLayoutContainer.cpp | 8 +- .../layouts/MessageLayoutContainer.hpp | 12 +- src/widgets/AttachedWindow.cpp | 8 +- src/widgets/BaseWidget.cpp | 13 - src/widgets/BaseWidget.hpp | 2 - src/widgets/BaseWindow.cpp | 551 +++++++++++------- src/widgets/BaseWindow.hpp | 9 +- src/widgets/Label.cpp | 19 +- src/widgets/TooltipEntryWidget.cpp | 1 + src/widgets/dialogs/SettingsDialog.cpp | 21 +- src/widgets/helper/Button.cpp | 54 +- src/widgets/helper/ChannelView.cpp | 29 +- src/widgets/helper/NotebookTab.cpp | 19 +- tests/src/MessageLayout.cpp | 2 +- 21 files changed, 466 insertions(+), 327 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 631b3eb1d8e..ef547e8bc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## Unversioned - Major: Release plugins alpha. (#5288) +- Major: Improve high-DPI support on Windows. (#4868) - Minor: Add option to customise Moderation buttons with images. (#5369) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334) diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index 6d5114423b0..93c69b603ed 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -1,11 +1,11 @@ * { - font-size: px; + font-size: 14px; font-family: "Segoe UI"; } QCheckBox::indicator { - width: px; - height: px; + width: 14px; + height: 14px; } chatterino--ComboBox { diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 13012957dc2..6fba9c6aff8 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -86,10 +86,6 @@ namespace { QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); #endif -#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - QApplication::setStyle(QStyleFactory::create("Fusion")); #ifndef Q_OS_MAC diff --git a/src/main.cpp b/src/main.cpp index ef59af0c529..8da92a45ca6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,11 +26,6 @@ using namespace chatterino; int main(int argc, char **argv) { - // TODO: This is a temporary fix (see #4552). -#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); -#endif - QApplication a(argc, argv); QCoreApplication::setApplicationName("chatterino"); diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index b11e82be197..3d5ab64e7f7 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -155,8 +155,8 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + auto image = this->emote_->images.getImageOrLoaded( + container.getImageScale()); if (image->isEmpty()) { return; @@ -210,7 +210,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto images = this->getLoadedImages(container.getScale()); + auto images = this->getLoadedImages(container.getImageScale()); if (images.empty()) { return; @@ -364,7 +364,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + this->emote_->images.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; @@ -798,7 +798,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { const auto &image = - this->images_.getImageOrLoaded(container.getScale()); + this->images_.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 126bb40c94d..8a20b05cc19 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -74,7 +74,8 @@ int MessageLayout::getWidth() const // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, +bool MessageLayout::layout(int width, float scale, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer) { // BenchmarkGuard benchmark("MessageLayout::layout()"); @@ -106,6 +107,8 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, // check if dpi changed layoutRequired |= this->scale_ != scale; this->scale_ = scale; + layoutRequired |= this->imageScale_ != imageScale; + this->imageScale_ = imageScale; if (!layoutRequired) { @@ -148,7 +151,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) bool hideSimilar = getSettings()->hideSimilar; bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage); - this->container_.beginLayout(width, this->scale_, messageFlags); + this->container_.beginLayout(width, this->scale_, this->imageScale_, + messageFlags); for (const auto &element : this->message_->elements) { @@ -288,16 +292,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) } // Create new buffer -#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) this->buffer_ = std::make_unique( int(width * painter.device()->devicePixelRatioF()), int(this->container_.getHeight() * painter.device()->devicePixelRatioF())); this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF()); -#else - this->buffer_ = std::make_unique( - width, std::max(16, this->container_.getHeight())); -#endif this->bufferValid_ = false; DebugCount::increase("message drawing buffers"); diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index f54f57d2a9a..01958ddf2ab 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -56,8 +56,8 @@ class MessageLayout MessageLayoutFlags flags; - bool layout(int width, float scale_, MessageElementFlags flags, - bool shouldInvalidateBuffer); + bool layout(int width, float scale_, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer); // Painting MessagePaintResult paint(const MessagePaintContext &ctx); @@ -128,6 +128,7 @@ class MessageLayout int currentLayoutWidth_ = -1; int layoutState_ = -1; float scale_ = -1; + float imageScale_ = -1.F; MessageElementFlags currentWordFlags_; #ifdef FOURTF diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 29d70e0a193..15a7d71afc3 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -30,7 +30,7 @@ constexpr const QMargins MARGIN{8, 4, 8, 4}; namespace chatterino { void MessageLayoutContainer::beginLayout(int width, float scale, - MessageFlags flags) + float imageScale, MessageFlags flags) { this->elements_.clear(); this->lines_.clear(); @@ -45,6 +45,7 @@ void MessageLayoutContainer::beginLayout(int width, float scale, this->width_ = width; this->height_ = 0; this->scale_ = scale; + this->imageScale_ = imageScale; this->flags_ = flags; auto mediumFontMetrics = getIApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale); @@ -526,6 +527,11 @@ float MessageLayoutContainer::getScale() const return this->scale_; } +float MessageLayoutContainer::getImageScale() const +{ + return this->imageScale_; +} + bool MessageLayoutContainer::isCollapsed() const { return this->isCollapsed_; diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index ed3c1a7a6b7..dde3f4d452f 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -32,7 +32,8 @@ struct MessageLayoutContainer { * This will reset all line calculations, and will be considered incomplete * until the accompanying end function has been called */ - void beginLayout(int width_, float scale_, MessageFlags flags_); + void beginLayout(int width, float scale, float imageScale, + MessageFlags flags); /** * Finish the layout process of this message @@ -146,6 +147,11 @@ struct MessageLayoutContainer { */ float getScale() const; + /** + * Returns the image scale + */ + float getImageScale() const; + /** * Returns true if this message is collapsed */ @@ -270,6 +276,10 @@ struct MessageLayoutContainer { // variables float scale_ = 1.F; + /** + * Scale factor for images + */ + float imageScale_ = 1.F; int width_ = 0; MessageFlags flags_{}; /** diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 5ce232a1f2f..b83afb65db5 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -270,20 +270,22 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) } float scale = 1.f; + float ourScale = 1.F; if (auto dpi = getWindowDpi(attached)) { scale = *dpi / 96.f; + ourScale = scale / this->devicePixelRatio(); for (auto w : this->ui_.split->findChildren()) { - w->setOverrideScale(scale); + w->setOverrideScale(ourScale); } - this->ui_.split->setOverrideScale(scale); + this->ui_.split->setOverrideScale(ourScale); } if (this->height_ != -1) { - this->ui_.split->setFixedWidth(int(this->width_ * scale)); + this->ui_.split->setFixedWidth(int(this->width_ * ourScale)); // offset int o = this->fullscreen_ ? 0 : 8; diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 5302d039745..5e2c932fd14 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -120,19 +120,6 @@ void BaseWidget::setScaleIndependantHeight(int value) QSize(this->scaleIndependantSize_.width(), value)); } -float BaseWidget::qtFontScale() const -{ - if (auto *window = dynamic_cast(this->window())) - { - // ensure no div by 0 - return this->scale() / std::max(0.01f, window->nativeScale_); - } - else - { - return this->scale(); - } -} - void BaseWidget::childEvent(QChildEvent *event) { if (event->added()) diff --git a/src/widgets/BaseWidget.hpp b/src/widgets/BaseWidget.hpp index 2e9c0472813..4fdc421cdde 100644 --- a/src/widgets/BaseWidget.hpp +++ b/src/widgets/BaseWidget.hpp @@ -34,8 +34,6 @@ class BaseWidget : public QWidget void setScaleIndependantWidth(int value); void setScaleIndependantHeight(int value); - float qtFontScale() const; - protected: void childEvent(QChildEvent *) override; void showEvent(QShowEvent *) override; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 81ed90b28be..5a1df70af2e 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -29,12 +29,163 @@ # pragma comment(lib, "Dwmapi.lib") # include - -# define WM_DPICHANGED 0x02E0 +# include #endif #include "widgets/helper/TitlebarButton.hpp" +namespace { + +#ifdef USEWINSDK + +// From kHiddenTaskbarSize in Firefox +constexpr UINT HIDDEN_TASKBAR_SIZE = 2; + +bool isWindows11OrGreater() +{ + static const bool result = [] { + // This calls RtlGetVersion under the hood so we don't have to. + // The micro version corresponds to dwBuildNumber. + auto version = QOperatingSystemVersion::current(); + return (version.majorVersion() > 10) || + (version.microVersion() >= 22000); + }(); + + return result; +} + +/// Finds the taskbar HWND on a specific monitor (or any) +HWND findTaskbarWindow(LPRECT rcMon = nullptr) +{ + HWND taskbar = nullptr; + RECT taskbarRect; + // return value of IntersectRect, unused + RECT intersectionRect; + + while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd", + nullptr)) != nullptr) + { + if (!rcMon) + { + // no monitor was specified, return the first encountered window + break; + } + if (GetWindowRect(taskbar, &taskbarRect) != 0 && + IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0) + { + // taskbar intersects with the monitor - this is the one + break; + } + } + + return taskbar; +} + +/// Gets the edge of the taskbar if it's automatically hidden +std::optional hiddenTaskbarEdge(LPRECT rcMon = nullptr) +{ + HWND taskbar = findTaskbarWindow(rcMon); + if (!taskbar) + { + return std::nullopt; + } + + APPBARDATA state = {sizeof(state), taskbar}; + APPBARDATA pos = {sizeof(pos), taskbar}; + + auto appBarState = + static_cast(SHAppBarMessage(ABM_GETSTATE, &state)); + if ((appBarState & ABS_AUTOHIDE) == 0) + { + return std::nullopt; + } + + if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0) + { + qCDebug(chatterinoApp) << "Failed to get taskbar pos"; + return ABE_BOTTOM; + } + + return pos.uEdge; +} + +/// @brief Gets the window borders for @a hwnd +/// +/// Each side of the returned RECT has the correct sign, so they can be added +/// to a window rect. +/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}. +RECT windowBordersFor(HWND hwnd, bool isMaximized) +{ + RECT margins{0, 0, 0, 0}; + + auto addBorders = isMaximized || isWindows11OrGreater(); + if (addBorders) + { + auto dpi = GetDpiForWindow(hwnd); + auto systemMetric = [&](auto index) { + if (dpi != 0) + { + return GetSystemMetricsForDpi(index, dpi); + } + return GetSystemMetrics(index); + }; + + auto paddedBorder = systemMetric(SM_CXPADDEDBORDER); + auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder; + auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder; + + margins.left += borderWidth; + margins.right -= borderWidth; + if (isMaximized) + { + margins.top += borderHeight; + } + margins.bottom -= borderHeight; + } + + if (isMaximized) + { + auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(mi); + auto *monitor = [&]() -> LPRECT { + if (GetMonitorInfo(hMonitor, &mi)) + { + return &mi.rcMonitor; + } + return nullptr; + }(); + + auto edge = hiddenTaskbarEdge(monitor); + if (edge) + { + switch (*edge) + { + case ABE_LEFT: + margins.left += HIDDEN_TASKBAR_SIZE; + break; + case ABE_RIGHT: + margins.right -= HIDDEN_TASKBAR_SIZE; + break; + case ABE_TOP: + margins.top += HIDDEN_TASKBAR_SIZE; + break; + case ABE_BOTTOM: + margins.bottom -= HIDDEN_TASKBAR_SIZE; + break; + default: + break; + } + } + } + + return margins; +} + +#endif + +} // namespace + namespace chatterino { BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) @@ -117,95 +268,80 @@ float BaseWindow::scale() const return std::max(0.01f, this->overrideScale().value_or(this->scale_)); } -float BaseWindow::qtFontScale() const -{ - return this->scale() / std::max(0.01F, this->nativeScale_); -} - void BaseWindow::init() { #ifdef USEWINSDK if (this->hasCustomWindowFrame()) { // CUSTOM WINDOW FRAME - QVBoxLayout *layout = new QVBoxLayout(); + auto *layout = new QVBoxLayout(this); this->ui_.windowLayout = layout; - layout->setContentsMargins(1, 1, 1, 1); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - this->setLayout(layout); + + if (!this->frameless_) { - if (!this->frameless_) - { - QHBoxLayout *buttonLayout = this->ui_.titlebarBox = - new QHBoxLayout(); - buttonLayout->setContentsMargins(0, 0, 0, 0); - layout->addLayout(buttonLayout); - - // title - Label *title = new Label; - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { - title->setText(text); - }); - - QSizePolicy policy(QSizePolicy::Ignored, - QSizePolicy::Preferred); - policy.setHorizontalStretch(1); - title->setSizePolicy(policy); - buttonLayout->addWidget(title); - this->ui_.titleLabel = title; - - // buttons - TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); - TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); - TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButtonStyle::Close); - - QObject::connect(_minButton, &TitleBarButton::leftClicked, this, - [this] { - this->setWindowState(Qt::WindowMinimized | - this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::leftClicked, this, - [this, _maxButton] { - this->setWindowState( - _maxButton->getButtonStyle() != + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); + buttonLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(buttonLayout); + + // title + Label *title = new Label; + QObject::connect(this, &QWidget::windowTitleChanged, + [title](const QString &text) { + title->setText(text); + }); + + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui_.titleLabel = title; + + // buttons + auto *minButton = new TitleBarButton; + minButton->setButtonStyle(TitleBarButtonStyle::Minimize); + auto *maxButton = new TitleBarButton; + maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); + auto *exitButton = new TitleBarButton; + exitButton->setButtonStyle(TitleBarButtonStyle::Close); + + QObject::connect(minButton, &TitleBarButton::leftClicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect( + maxButton, &TitleBarButton::leftClicked, this, + [this, maxButton] { + this->setWindowState(maxButton->getButtonStyle() != TitleBarButtonStyle::Maximize ? Qt::WindowActive : Qt::WindowMaximized); - }); - QObject::connect(_exitButton, &TitleBarButton::leftClicked, - this, [this] { - this->close(); - }); - - this->ui_.titlebarButtons = new TitleBarButtons( - this, _minButton, _maxButton, _exitButton); - - this->ui_.buttons.push_back(_minButton); - this->ui_.buttons.push_back(_maxButton); - this->ui_.buttons.push_back(_exitButton); - - // buttonLayout->addStretch(1); - buttonLayout->addWidget(_minButton); - buttonLayout->addWidget(_maxButton); - buttonLayout->addWidget(_exitButton); - buttonLayout->setSpacing(0); - } + }); + QObject::connect(exitButton, &TitleBarButton::leftClicked, this, + [this] { + this->close(); + }); + + this->ui_.titlebarButtons = + new TitleBarButtons(this, minButton, maxButton, exitButton); + + this->ui_.buttons.push_back(minButton); + this->ui_.buttons.push_back(maxButton); + this->ui_.buttons.push_back(exitButton); + + buttonLayout->addWidget(minButton); + buttonLayout->addWidget(maxButton); + buttonLayout->addWidget(exitButton); + buttonLayout->setSpacing(0); } + this->ui_.layoutBase = new BaseWidget(this); this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1); layout->addWidget(this->ui_.layoutBase); } - -// DPI -// auto dpi = getWindowDpi(this->safeHWND()); - -// if (dpi) { -// this->scale = dpi.value() / 96.f; -// } #endif // TopMost flag overrides setting @@ -571,29 +707,8 @@ void BaseWindow::resizeEvent(QResizeEvent *) } #ifdef USEWINSDK - if (this->hasCustomWindowFrame() && !this->isResizeFixing_) - { - this->isResizeFixing_ = true; - QTimer::singleShot(50, this, [this] { - auto hwnd = this->safeHWND(); - if (!hwnd) - { - this->isResizeFixing_ = false; - return; - } - RECT rect; - ::GetWindowRect(*hwnd, &rect); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - QTimer::singleShot(10, this, [this] { - this->isResizeFixing_ = false; - }); - }); - } - this->calcButtonsSizes(); + this->updateRealSize(); #endif } @@ -655,10 +770,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, switch (msg->message) { - case WM_DPICHANGED: - returnValue = this->handleDPICHANGED(msg); - break; - case WM_SHOWWINDOW: returnValue = this->handleSHOWWINDOW(msg); break; @@ -697,12 +808,15 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, { *result = 0; returnValue = true; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + + // TODO(nerix): use TrackMouseEvent here this->ui_.titlebarButtons->hover(msg->wParam, globalPos); this->lastEventWasNcMouseMove_ = true; } @@ -748,12 +862,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, *result = 0; auto ht = msg->wParam; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + if (msg->message == WM_NCLBUTTONDOWN) { this->ui_.titlebarButtons->mousePress(ht, globalPos); @@ -784,7 +900,7 @@ void BaseWindow::scaleChangedEvent(float scale) #endif this->setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); + getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale())); } void BaseWindow::paintEvent(QPaintEvent *) @@ -802,10 +918,9 @@ void BaseWindow::paintEvent(QPaintEvent *) void BaseWindow::updateScale() { - auto scale = - this->nativeScale_ * (this->flags_.has(DisableCustomScaling) - ? 1 - : getSettings()->getClampedUiScale()); + auto scale = this->flags_.has(DisableCustomScaling) + ? 1 + : getSettings()->getClampedUiScale(); this->setScale(scale); @@ -815,6 +930,22 @@ void BaseWindow::updateScale() } } +#ifdef USEWINSDK +void BaseWindow::updateRealSize() +{ + auto hwnd = this->safeHWND(); + if (!hwnd) + { + return; + } + + RECT real; + ::GetWindowRect(*hwnd, &real); + this->realBounds_ = QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); +} +#endif + void BaseWindow::calcButtonsSizes() { if (!this->shown_) @@ -846,34 +977,28 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter) { QColor bg = this->overrideBackgroundColor_.value_or( this->theme->window.background); - painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3), - bg); + if (this->isMaximized_) + { + painter.fillRect(this->rect(), bg); + } + else + { + // Draw a border that's exactly 1px wide + // + // There is a bug where the border can get px wide while dragging. + // this "fixes" itself when deselecting the window. + auto dpr = this->devicePixelRatio(); + if (dpr != 1) + { + painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr)); + } + painter.fillRect(1, 1, this->realBounds_.width() - 2, + this->realBounds_.height() - 2, bg); + } } #endif } -bool BaseWindow::handleDPICHANGED(MSG *msg) -{ -#ifdef USEWINSDK - int dpi = HIWORD(msg->wParam); - - float _scale = dpi / 96.f; - - auto *prcNewWindow = reinterpret_cast(msg->lParam); - SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, - prcNewWindow->right - prcNewWindow->left, - prcNewWindow->bottom - prcNewWindow->top, - SWP_NOZORDER | SWP_NOACTIVATE); - - this->nativeScale_ = _scale; - this->updateScale(); - - return true; -#else - return false; -#endif -} - bool BaseWindow::handleSHOWWINDOW(MSG *msg) { #ifdef USEWINSDK @@ -883,16 +1008,6 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) return true; } - if (auto dpi = getWindowDpi(msg->hwnd)) - { - float currentScale = (float)dpi.value() / 96.F; - if (currentScale != this->nativeScale_) - { - this->nativeScale_ = currentScale; - this->updateScale(); - } - } - if (!this->shown_) { this->shown_ = true; @@ -906,14 +1021,12 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) if (!this->initalBounds_.isNull()) { - ::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(), - this->initalBounds_.y(), this->initalBounds_.width(), - this->initalBounds_.height(), - SWP_NOZORDER | SWP_NOACTIVATE); + this->setGeometry(this->initalBounds_); this->currentBounds_ = this->initalBounds_; } this->calcButtonsSizes(); + this->updateRealSize(); } return true; @@ -929,23 +1042,54 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) #endif { #ifdef USEWINSDK - if (this->hasCustomWindowFrame()) + if (!this->hasCustomWindowFrame()) { - if (msg->wParam == TRUE) - { - // remove 1 extra pixel on top of custom frame - auto *ncp = reinterpret_cast(msg->lParam); - if (ncp) - { - ncp->lppos->flags |= SWP_NOREDRAW; - ncp->rgrc[0].top -= 1; - } - } + return false; + } + if (msg->wParam != TRUE) + { *result = 0; return true; } - return false; + + auto *params = reinterpret_cast(msg->lParam); + auto *r = ¶ms->rgrc[0]; + + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 && + (wp.showCmd == SW_SHOWMAXIMIZED); + + auto borders = windowBordersFor(msg->hwnd, this->isMaximized_); + r->left += borders.left; + r->top += borders.top; + r->right += borders.right; + r->bottom += borders.bottom; + + if (borders.left != 0 || borders.top != 0 || borders.right != 0 || + borders.bottom != 0) + { + // We added borders -> we changed the rect, so we can't return + // WVR_VALIDRECTS + *result = 0; + return true; + } + + // This is an attempt at telling Windows to not redraw (or at least to do a + // better job at redrawing) the window. There is a long list of tricks + // people tried to prevent this at + // https://stackoverflow.com/q/53000291/16300717 + // + // We set the source and destination rectangles to a 1x1 rectangle at the + // top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve + // some parts of the window image. + QPoint fixed = {r->left, r->top}; + params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + *result = WVR_VALIDRECTS; + + return true; #else return false; #endif @@ -962,28 +1106,11 @@ bool BaseWindow::handleSIZE(MSG *msg) } else if (this->hasCustomWindowFrame()) { - if (msg->wParam == SIZE_MAXIMIZED) - { - auto offset = - int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96); - - this->ui_.windowLayout->setContentsMargins(offset, offset, - offset, offset); - } - else - { - this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); - } - this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED; if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->currentBounds_ = - QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); + this->currentBounds_ = this->geometry(); } this->useNextBounds_.stop(); @@ -993,6 +1120,12 @@ bool BaseWindow::handleSIZE(MSG *msg) // the minimize button, so we have to emulate it. this->ui_.titlebarButtons->leave(); } + + RECT real; + ::GetWindowRect(msg->hwnd, &real); + this->realBounds_ = + QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); } } return false; @@ -1006,11 +1139,7 @@ bool BaseWindow::handleMOVE(MSG *msg) #ifdef USEWINSDK if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->nextBounds_ = QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); - + this->nextBounds_ = this->geometry(); this->useNextBounds_.start(10); } #endif @@ -1024,31 +1153,37 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) #endif { #ifdef USEWINSDK - const LONG border_width = 8; // in pixels - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); + const LONG borderWidth = 8; // in device independent pixels + + auto rect = this->rect(); + + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); + QPoint point(p.x, p.y); + point /= this->devicePixelRatio(); - QPoint point(x - winrect.left, y - winrect.top); + auto x = point.x(); + auto y = point.y(); if (this->hasCustomWindowFrame()) { *result = 0; - bool resizeWidth = minimumWidth() != maximumWidth(); - bool resizeHeight = minimumHeight() != maximumHeight(); + bool resizeWidth = + minimumWidth() != maximumWidth() && !this->isMaximized(); + bool resizeHeight = + minimumHeight() != maximumHeight() && !this->isMaximized(); if (resizeWidth) { // left border - if (x < winrect.left + border_width) + if (x < rect.left() + borderWidth) { *result = HTLEFT; } // right border - if (x >= winrect.right - border_width) + if (x >= rect.right() - borderWidth) { *result = HTRIGHT; } @@ -1056,12 +1191,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeHeight) { // bottom border - if (y >= winrect.bottom - border_width) + if (y >= rect.bottom() - borderWidth) { *result = HTBOTTOM; } // top border - if (y < winrect.top + border_width) + if (y < rect.top() + borderWidth) { *result = HTTOP; } @@ -1069,26 +1204,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeWidth && resizeHeight) { // bottom left corner - if (x >= winrect.left && x < winrect.left + border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMLEFT; } // bottom right corner - if (x < winrect.right && x >= winrect.right - border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMRIGHT; } // top left corner - if (x >= winrect.left && x < winrect.left + border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPLEFT; } // top right corner - if (x < winrect.right && x >= winrect.right - border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPRIGHT; } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index b9f21b08d3e..02101cb6dc0 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -75,7 +75,6 @@ class BaseWindow : public BaseWidget bool applyLastBoundsCheck(); float scale() const override; - float qtFontScale() const; /// @returns true if the window is the top-most window. /// Either #setTopMost was called or the `TopMost` flag is set which overrides this @@ -132,7 +131,6 @@ class BaseWindow : public BaseWidget void drawCustomWindowFrame(QPainter &painter); void onFocusLost(); - bool handleDPICHANGED(MSG *msg); bool handleSHOWWINDOW(MSG *msg); bool handleSIZE(MSG *msg); bool handleMOVE(MSG *msg); @@ -149,8 +147,6 @@ class BaseWindow : public BaseWidget bool frameless_; bool shown_ = false; FlagsEnum flags_; - float nativeScale_ = 1; - bool isResizeFixing_ = false; bool isTopMost_ = false; struct { @@ -168,6 +164,7 @@ class BaseWindow : public BaseWidget widgets::BoundsChecking lastBoundsCheckMode_ = widgets::BoundsChecking::Off; #ifdef USEWINSDK + void updateRealSize(); /// @brief Returns the HWND of this window if it has one /// /// A QWidget only has an HWND if it has been created. Before that, @@ -193,6 +190,10 @@ class BaseWindow : public BaseWidget QTimer useNextBounds_; bool isNotMinimizedOrMaximized_{}; bool lastEventWasNcMouseMove_ = false; + /// The real bounds of the window as returned by + /// GetWindowRect. Used for drawing. + QRect realBounds_; + bool isMaximized_ = false; #endif pajlada::Signals::SignalHolder connections_; diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index 37bd9df3601..1d3e7067f6c 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -88,23 +88,10 @@ void Label::paintEvent(QPaintEvent *) { QPainter painter(this); - qreal deviceDpi = -#ifdef Q_OS_WIN - this->devicePixelRatioF(); -#else - 1.0; -#endif - QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.01F, static_cast(this->logicalDpiX() * deviceDpi))); - painter.setFont(getIApp()->getFonts()->getFont( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.02F, static_cast(this->logicalDpiX() * deviceDpi)))); + this->getFontStyle(), this->scale()); + painter.setFont( + getIApp()->getFonts()->getFont(this->getFontStyle(), this->scale())); int offset = this->getOffset(); diff --git a/src/widgets/TooltipEntryWidget.cpp b/src/widgets/TooltipEntryWidget.cpp index 7ef7274e385..0f0ac5336b5 100644 --- a/src/widgets/TooltipEntryWidget.cpp +++ b/src/widgets/TooltipEntryWidget.cpp @@ -86,6 +86,7 @@ bool TooltipEntryWidget::refreshPixmap() this->attemptRefresh_ = true; return false; } + pixmap->setDevicePixelRatio(this->devicePixelRatio()); if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 62d459e222f..8b56f4903cd 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -47,7 +47,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) this->resize(915, 600); this->themeChangedEvent(); - this->scaleChangedEvent(this->scale()); + QFile styleFile(":/qss/settings.qss"); + styleFile.open(QFile::ReadOnly); + QString stylesheet = QString::fromUtf8(styleFile.readAll()); + this->setStyleSheet(stylesheet); this->initUi(); this->addTabs(); @@ -396,25 +399,19 @@ void SettingsDialog::refresh() void SettingsDialog::scaleChangedEvent(float newDpi) { - QFile file(":/qss/settings.qss"); - file.open(QFile::ReadOnly); - QString styleSheet = QLatin1String(file.readAll()); - styleSheet.replace("", QString::number(int(14 * newDpi))); - styleSheet.replace("", QString::number(int(14 * newDpi))); + assert(newDpi == 1.F && + "Scaling is disabled for the settings dialog - its scale should " + "always be 1"); for (SettingsDialogTab *tab : this->tabs_) { - tab->setFixedHeight(int(30 * newDpi)); + tab->setFixedHeight(30); } - this->setStyleSheet(styleSheet); - if (this->ui_.tabContainerContainer) { - this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi)); + this->ui_.tabContainerContainer->setFixedWidth(150); } - - this->dpi_ = newDpi; } void SettingsDialog::themeChangedEvent() diff --git a/src/widgets/helper/Button.cpp b/src/widgets/helper/Button.cpp index 08dde78e146..0574326666a 100644 --- a/src/widgets/helper/Button.cpp +++ b/src/widgets/helper/Button.cpp @@ -8,26 +8,44 @@ #include #include -namespace chatterino { namespace { - // returns a new resized image or the old one if the size didn't change - auto resizePixmap(const QPixmap ¤t, const QPixmap resized, - const QSize &size) -> QPixmap +QSizeF deviceIndependentSize(const QPixmap &pixmap) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0) + return QSizeF(pixmap.width(), pixmap.height()) / pixmap.devicePixelRatio(); +#else + return pixmap.deviceIndependentSize(); +#endif +} + +/** + * Resizes a pixmap to a desired size. + * Does nothing if the target pixmap is already sized correctly. + * + * @param target The target pixmap. + * @param source The unscaled pixmap. + * @param size The desired device independent size. + * @param dpr The device pixel ratio of the target area. The size of the target in pixels will be `size * dpr`. + */ +void resizePixmap(QPixmap &target, const QPixmap &source, const QSize &size, + qreal dpr) +{ + if (deviceIndependentSize(target) == size) { - if (resized.size() == size) - { - return resized; - } - else - { - return current.scaled(size, Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - } + return; } + QPixmap resized = source; + resized.setDevicePixelRatio(dpr); + target = resized.scaled(size * dpr, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); +} + } // namespace +namespace chatterino { + Button::Button(BaseWidget *parent) : BaseWidget(parent) { @@ -47,6 +65,12 @@ void Button::setMouseEffectColor(std::optional color) void Button::setPixmap(const QPixmap &_pixmap) { + // Avoid updates if the pixmap didn't change + if (_pixmap.cacheKey() == this->pixmap_.cacheKey()) + { + return; + } + this->pixmap_ = _pixmap; this->resizedPixmap_ = {}; this->update(); @@ -158,8 +182,8 @@ void Button::paintButton(QPainter &painter) QRect rect = this->rect(); - this->resizedPixmap_ = - resizePixmap(this->pixmap_, this->resizedPixmap_, rect.size()); + resizePixmap(this->resizedPixmap_, this->pixmap_, rect.size(), + this->devicePixelRatio()); int margin = this->height() < 22 * this->scale() ? 3 : 6; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 819b7d0c0bf..ed636cca4b6 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -615,7 +615,7 @@ void ChannelView::scaleChangedEvent(float scale) if (this->goToBottom_) { - auto factor = this->qtFontScale(); + auto factor = this->scale(); #ifdef Q_OS_MACOS factor = scale * 80.F / std::max( @@ -703,8 +703,10 @@ void ChannelView::layoutVisibleMessages( { const auto &message = messages[i]; - redrawRequired |= message->layout(layoutWidth, this->scale(), flags, - this->bufferInvalidationQueued_); + redrawRequired |= message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), + flags, this->bufferInvalidationQueued_); y += message->getHeight(); } @@ -738,7 +740,10 @@ void ChannelView::updateScrollbar( { auto *message = messages[i].get(); - message->layout(layoutWidth, this->scale(), flags, false); + message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), flags, + false); h -= message->getHeight(); @@ -1720,9 +1725,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i - 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i - 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i - 1]->getHeight(); } @@ -1755,9 +1762,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i + 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i + 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i + 1]->getHeight(); diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index da04562fae2..f7cb2b3a0f3 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -27,15 +27,6 @@ namespace chatterino { namespace { - qreal deviceDpi(QWidget *widget) - { -#ifdef Q_OS_WIN - return widget->devicePixelRatioF(); -#else - return 1.0; -#endif - } - // Translates the given rectangle by an amount in the direction to appear like the tab is selected. // For example, if location is Top, the rectangle will be translated in the negative Y direction, // or "up" on the screen, by amount. @@ -196,8 +187,8 @@ int NotebookTab::normalTabWidth() float scale = this->scale(); int width; - auto metrics = getIApp()->getFonts()->getFontMetrics( - FontStyle::UiTabs, float(qreal(this->scale()) * deviceDpi(this))); + QFontMetrics metrics = + getIApp()->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); if (this->hasXButton()) { @@ -439,11 +430,9 @@ void NotebookTab::paintEvent(QPaintEvent *) QPainter painter(this); float scale = this->scale(); - auto div = std::max(0.01f, this->logicalDpiX() * deviceDpi(this)); - painter.setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, scale * 96.f / div)); + painter.setFont(app->getFonts()->getFont(FontStyle::UiTabs, scale)); QFontMetrics metrics = - app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale * 96.f / div); + app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); int height = int(scale * NOTEBOOK_TAB_HEIGHT); diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index ab9a294c9b8..8533b87b871 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -63,7 +63,7 @@ class MessageLayoutTest builder.append( std::make_unique(text, MessageElementFlag::Text)); this->layout = std::make_unique(builder.release()); - this->layout->layout(WIDTH, 1, MessageElementFlag::Text, false); + this->layout->layout(WIDTH, 1, 1, MessageElementFlag::Text, false); } MockApplication mockApplication; From 3d5acff907806c75095068d6ed84c9ccd2f78300 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 12 May 2024 14:37:47 +0200 Subject: [PATCH 055/105] fix: update color of usernames & boldness of usernames on the fly (#5300) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 5 +- src/messages/MessageElement.cpp | 32 +++++ src/messages/MessageElement.hpp | 44 ++++++- .../layouts/MessageLayoutContainer.cpp | 4 +- src/providers/twitch/TwitchMessageBuilder.cpp | 120 ++++++------------ src/singletons/WindowManager.cpp | 10 +- 7 files changed, 120 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef547e8bc18..6a9654f3ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868) - Minor: Add option to customise Moderation buttons with images. (#5369) +- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index e1719454971..1ffd5ba28db 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -763,10 +763,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) auto &&textColor = this->textColor_; if (string.startsWith('@')) { - this->emplace(string, MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold); - this->emplace(string, MessageElementFlag::NonBoldUsername, - textColor); + this->emplace(string, textColor, textColor); } else { diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 3d5ab64e7f7..17de1270608 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -703,6 +703,38 @@ Link LinkElement::getLink() const return {Link::Url, this->linkInfo_.url()}; } +MentionElement::MentionElement(const QString &name, MessageColor fallbackColor_, + MessageColor userColor_) + : TextElement(name, {MessageElementFlag::Text, MessageElementFlag::Mention}) + , fallbackColor(fallbackColor_) + , userColor(userColor_) +{ +} + +void MentionElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (getSettings()->colorUsernames) + { + this->color_ = this->userColor; + } + else + { + this->color_ = this->fallbackColor; + } + + if (getSettings()->boldUsernames) + { + this->style_ = FontStyle::ChatMediumBold; + } + else + { + this->style_ = FontStyle::ChatMedium; + } + + TextElement::addToContainer(container, flags); +} + // TIMESTAMP TimestampElement::TimestampElement(QTime time) : MessageElement(MessageElementFlag::Timestamp) diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index b57bab752c4..2c1e98f4e89 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -133,9 +133,10 @@ enum class MessageElementFlag : int64_t { // needed Collapsed = (1LL << 26), - // used for dynamic bold usernames - BoldUsername = (1LL << 27), - NonBoldUsername = (1LL << 28), + // A mention of a username that isn't the author of the message + Mention = (1LL << 27), + + // Unused = (1LL << 28), // used to check if links should be lowercased LowercaseLinks = (1LL << 29), @@ -236,7 +237,6 @@ class TextElement : public MessageElement protected: QStringList words_; -private: MessageColor color_; FontStyle style_; }; @@ -301,6 +301,42 @@ class LinkElement : public TextElement QStringList original_; }; +/** + * @brief Contains a username mention. + * + * Examples of mentions: + * V + * 13:37 pajlada: hello @forsen + * + * V V + * 13:37 The moderators of this channel are: forsen, nuuls + */ +class MentionElement : public TextElement +{ +public: + MentionElement(const QString &name, MessageColor fallbackColor_, + MessageColor userColor_); + ~MentionElement() override = default; + MentionElement(const MentionElement &) = delete; + MentionElement(MentionElement &&) = delete; + MentionElement &operator=(const MentionElement &) = delete; + MentionElement &operator=(MentionElement &&) = delete; + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + +private: + /** + * The color of the element in case the "Colorize @usernames" is disabled + **/ + MessageColor fallbackColor; + + /** + * The color of the element in case the "Colorize @usernames" is enabled + **/ + MessageColor userColor; +}; + // contains emote data and will pick the emote based on : // a) are images for the emote type enabled // b) which size it wants diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 15a7d71afc3..e5e53f360e0 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -756,9 +756,7 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex) const auto neutral = isNeutral(element->getText()); const auto neutralOrUsername = - neutral || - element->getFlags().hasAny({MessageElementFlag::BoldUsername, - MessageElementFlag::NonBoldUsername}); + neutral || element->getFlags().has(MessageElementFlag::Mention); if (neutral && ((this->first == FirstWord::RTL && !this->wasPrevReversed_) || diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 524d0375de5..3dfd39720ec 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -51,6 +51,8 @@ using namespace chatterino::literals; namespace { +const QColor AUTOMOD_USER_COLOR{"blue"}; + using namespace std::chrono_literals; const QString regexHelpString("(\\w+)[.,!?;:]*?$"); @@ -756,7 +758,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) QString username = match.captured(1); auto originalTextColor = textColor; - if (this->twitchChannel != nullptr && getSettings()->colorUsernames) + if (this->twitchChannel != nullptr) { if (auto userColor = this->twitchChannel->getUserColor(username); @@ -767,21 +769,17 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } auto prefixedUsername = '@' + username; - this->emplace(prefixedUsername, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(prefixedUsername, - MessageElementFlag::NonBoldUsername, - textColor) + auto remainder = string.remove(prefixedUsername); + this->emplace(prefixedUsername, originalTextColor, + textColor) ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); + ->setTrailingSpace(remainder.isEmpty()); - this->emplace(string.remove(prefixedUsername), - MessageElementFlag::Text, - originalTextColor); + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } return; } @@ -797,30 +795,23 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) { auto originalTextColor = textColor; - if (getSettings()->colorUsernames) + if (auto userColor = this->twitchChannel->getUserColor(username); + userColor.isValid()) { - if (auto userColor = - this->twitchChannel->getUserColor(username); - userColor.isValid()) - { - textColor = userColor; - } + textColor = userColor; } - this->emplace(username, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) + auto remainder = string.remove(username); + this->emplace(username, originalTextColor, + textColor) ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); + ->setTrailingSpace(remainder.isEmpty()); - this->emplace( - username, MessageElementFlag::NonBoldUsername, textColor) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(string.remove(username), - MessageElementFlag::Text, - originalTextColor); + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } return; } @@ -1821,7 +1812,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, MessageColor color = MessageColor::System; - if (tc && getSettings()->colorUsernames) + if (tc) { if (auto userColor = tc->getUserColor(username); userColor.isValid()) @@ -1830,14 +1821,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, } } - builder - ->emplace(username, MessageElementFlag::BoldUsername, - color, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - builder - ->emplace(username, - MessageElementFlag::NonBoldUsername, color) + builder->emplace(username, MessageColor::System, color) ->setLink({Link::UserInfo, username}) ->setTrailingSpace(false); } @@ -1873,7 +1857,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage( MessageColor color = MessageColor::System; - if (tc && getSettings()->colorUsernames) + if (tc) { if (auto userColor = tc->getUserColor(user.userLogin); userColor.isValid()) @@ -1883,14 +1867,8 @@ void TwitchMessageBuilder::listOfUsersSystemMessage( } builder - ->emplace(user.userName, - MessageElementFlag::BoldUsername, color, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, user.userLogin}) - ->setTrailingSpace(false); - builder - ->emplace(user.userName, - MessageElementFlag::NonBoldUsername, color) + ->emplace(user.userName, MessageColor::System, + color) ->setLink({Link::UserInfo, user.userLogin}) ->setTrailingSpace(false); } @@ -1960,12 +1938,8 @@ MessagePtr TwitchMessageBuilder::makeAutomodInfoMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); + builder.emplace("AutoMod:", AUTOMOD_USER_COLOR, + AUTOMOD_USER_COLOR); switch (action.type) { case AutomodInfoAction::OnHold: { @@ -2019,12 +1993,8 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); + builder2.emplace("AutoMod:", AUTOMOD_USER_COLOR, + AUTOMOD_USER_COLOR); // AutoMod header message builder.emplace( ("Held a message for reason: " + action.reason + @@ -2072,14 +2042,8 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( // sender username builder2 - .emplace( - action.target.displayName + ":", MessageElementFlag::BoldUsername, - MessageColor(action.target.color), FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.target.login}); - builder2 - .emplace(action.target.displayName + ":", - MessageElementFlag::NonBoldUsername, - MessageColor(action.target.color)) + .emplace(action.target.displayName + ":", + MessageColor::Text, action.target.color) ->setLink({Link::UserInfo, action.target.login}); // sender's message caught by AutoMod builder2.emplace(action.message, MessageElementFlag::Text, @@ -2275,17 +2239,9 @@ std::pair TwitchMessageBuilder::makeLowTrustUserMessage( appendBadges(&builder2, action.senderBadges, {}, twitchChannel); // sender username - builder2 - .emplace(action.suspiciousUserDisplayName + ":", - MessageElementFlag::BoldUsername, - MessageColor(action.suspiciousUserColor), - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.suspiciousUserLogin}); - builder2 - .emplace(action.suspiciousUserDisplayName + ":", - MessageElementFlag::NonBoldUsername, - MessageColor(action.suspiciousUserColor)) - ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder2.emplace(action.suspiciousUserDisplayName + ":", + MessageColor::Text, + action.suspiciousUserColor); // sender's message caught by AutoMod for (const auto &fragment : action.fragments) diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 56bea50ae69..63bc63b366b 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -108,7 +108,6 @@ WindowManager::WindowManager(const Paths &paths) this->wordFlagsListener_.addSetting(settings->showBadgesFfz); this->wordFlagsListener_.addSetting(settings->showBadgesSevenTV); this->wordFlagsListener_.addSetting(settings->enableEmoteImages); - this->wordFlagsListener_.addSetting(settings->boldUsernames); this->wordFlagsListener_.addSetting(settings->lowercaseDomains); this->wordFlagsListener_.addSetting(settings->showReplyButton); this->wordFlagsListener_.setCB([this] { @@ -182,8 +181,6 @@ void WindowManager::updateWordTypeMask() // misc flags.set(MEF::AlwaysShow); flags.set(MEF::Collapsed); - flags.set(settings->boldUsernames ? MEF::BoldUsername - : MEF::NonBoldUsername); flags.set(MEF::LowercaseLinks, settings->lowercaseDomains); flags.set(MEF::ChannelPointReward); @@ -422,6 +419,13 @@ void WindowManager::initialize(Settings &settings, const Paths &paths) this->forceLayoutChannelViews(); }); + settings.colorUsernames.connect([this](auto, auto) { + this->forceLayoutChannelViews(); + }); + settings.boldUsernames.connect([this](auto, auto) { + this->forceLayoutChannelViews(); + }); + this->initialized_ = true; } From 2ad45bc2880192f80614a5f6a198e87151ff37e2 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 22:46:26 +0200 Subject: [PATCH 056/105] fix: don't use DPI aware functions on Qt 5 (Windows 7/8) (#5391) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9654f3ffe..0e6b65f6132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unversioned - Major: Release plugins alpha. (#5288) -- Major: Improve high-DPI support on Windows. (#4868) +- Major: Improve high-DPI support on Windows. (#4868, #5391) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 5a1df70af2e..08a13007ab9 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -121,12 +121,19 @@ RECT windowBordersFor(HWND hwnd, bool isMaximized) auto addBorders = isMaximized || isWindows11OrGreater(); if (addBorders) { + // GetDpiForWindow and GetSystemMetricsForDpi are only supported on + // Windows 10 and later. Qt 6 requires Windows 10. +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto dpi = GetDpiForWindow(hwnd); +# endif + auto systemMetric = [&](auto index) { +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (dpi != 0) { return GetSystemMetricsForDpi(index, dpi); } +# endif return GetSystemMetrics(index); }; From fdecb4a39f3e0340fe555797030454986b50be78 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 13 May 2024 20:00:50 +0200 Subject: [PATCH 057/105] revert: use max(minimum, min(bottom, value)) over clamp(..) (#5393) --- CHANGELOG.md | 2 +- src/widgets/Scrollbar.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e6b65f6132..66d5e9dbaa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) -- Dev: Refactor and document `Scrollbar`. (#5334) +- Dev: Refactor and document `Scrollbar`. (#5334, #5393) ## 2.5.1 diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index e1492b67321..827ea645bec 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -153,7 +153,9 @@ void Scrollbar::setPageSize(qreal value) void Scrollbar::setDesiredValue(qreal value, bool animated) { - value = std::clamp(value, this->minimum_, this->getBottom()); + // this can't use std::clamp, because minimum_ < getBottom() isn't always + // true, which is a precondition for std::clamp + value = std::max(this->minimum_, std::min(this->getBottom(), value)); if (areClose(this->currentValue_, value)) { // value has not changed From 614a1c469f75bdebbf03c7ee62901f4d62a180e1 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 18 May 2024 11:56:36 +0200 Subject: [PATCH 058/105] Added `flags.action` filter variable (#5397) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/controllers/filters/lang/Filter.cpp | 1 + src/controllers/filters/lang/Filter.hpp | 1 + src/controllers/filters/lang/Tokenizer.hpp | 1 + 4 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d5e9dbaa6..139b7f850aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Major: Improve high-DPI support on Windows. (#4868, #5391) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) +- Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 9c3ecb0228b..1a246166198 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -93,6 +93,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) {"channel.name", m->channelName}, {"channel.watching", watching}, + {"flags.action", m->flags.has(MessageFlag::Action)}, {"flags.highlighted", m->flags.has(MessageFlag::Highlighted)}, {"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)}, {"flags.sub_message", m->flags.has(MessageFlag::Subscription)}, diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp index 01d7a765e9d..7a0f44805a1 100644 --- a/src/controllers/filters/lang/Filter.hpp +++ b/src/controllers/filters/lang/Filter.hpp @@ -32,6 +32,7 @@ static const QMap MESSAGE_TYPING_CONTEXT = { {"channel.name", Type::String}, {"channel.watching", Type::Bool}, {"channel.live", Type::Bool}, + {"flags.action", Type::Bool}, {"flags.highlighted", Type::Bool}, {"flags.points_redeemed", Type::Bool}, {"flags.sub_message", Type::Bool}, diff --git a/src/controllers/filters/lang/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp index 6ca9d373ceb..ced78c5d2e7 100644 --- a/src/controllers/filters/lang/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -18,6 +18,7 @@ static const QMap validIdentifiersMap = { {"channel.name", "channel name"}, {"channel.watching", "/watching channel?"}, {"channel.live", "channel live?"}, + {"flags.action", "action/me message?"}, {"flags.highlighted", "highlighted?"}, {"flags.points_redeemed", "redeemed points?"}, {"flags.sub_message", "sub/resub message?"}, From 3ed1c0f7a4e97b778e73c1a55748d30863ce2b49 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 18 May 2024 12:34:36 +0200 Subject: [PATCH 059/105] fix: don't attempt to scale windows opted out of scaling (#5400) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 22 ++++++++++++++++++++-- src/widgets/BaseWindow.hpp | 2 ++ src/widgets/dialogs/SettingsDialog.cpp | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 139b7f850aa..edba34f08a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) -- Dev: Use Qt's high DPI scaling. (#4868) +- Dev: Use Qt's high DPI scaling. (#4868, #5400) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 08a13007ab9..bc8767e4999 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -931,9 +931,27 @@ void BaseWindow::updateScale() this->setScale(scale); - for (auto *child : this->findChildren()) + BaseWindow::applyScaleRecursive(this, scale); +} + +// NOLINTNEXTLINE(misc-no-recursion) +void BaseWindow::applyScaleRecursive(QObject *root, float scale) +{ + for (QObject *obj : root->children()) { - child->setScale(scale); + auto *base = dynamic_cast(obj); + if (base) + { + auto *window = dynamic_cast(obj); + if (window) + { + // stop here, the window will get the event as well (via uiScale) + continue; + } + base->setScale(scale); + } + + applyScaleRecursive(obj, scale); } } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 02101cb6dc0..46ff56e7e38 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -131,6 +131,8 @@ class BaseWindow : public BaseWidget void drawCustomWindowFrame(QPainter &painter); void onFocusLost(); + static void applyScaleRecursive(QObject *root, float scale); + bool handleSHOWWINDOW(MSG *msg); bool handleSIZE(MSG *msg); bool handleMOVE(MSG *msg); diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 8b56f4903cd..2a71593eccf 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -196,7 +196,7 @@ void SettingsDialog::filterElements(const QString &text) auto *item = this->ui_.tabContainer->itemAt(i); if (auto *x = dynamic_cast(item); x) { - x->changeSize(10, shouldShowSpace ? int(16 * this->scale()) : 0); + x->changeSize(10, shouldShowSpace ? 16 : 0); shouldShowSpace = false; } else if (item->widget()) From 869562263ff4563d5c3804268afdc1de2f225371 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 13:29:10 +0200 Subject: [PATCH 060/105] chore(deps): bump ZedThree/clang-tidy-review from 0.18.0 to 0.19.0 (#5394) Bumps [ZedThree/clang-tidy-review](https://github.com/zedthree/clang-tidy-review) from 0.18.0 to 0.19.0. - [Release notes](https://github.com/zedthree/clang-tidy-review/releases) - [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md) - [Commits](https://github.com/zedthree/clang-tidy-review/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: ZedThree/clang-tidy-review dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/clang-tidy.yml | 4 ++-- .github/workflows/post-clang-tidy-review.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index cf47eacaf11..785fb593242 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -40,7 +40,7 @@ jobs: - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.18.0 + uses: ZedThree/clang-tidy-review@v0.19.0 with: build_dir: build-clang-tidy config_file: ".clang-tidy" @@ -62,4 +62,4 @@ jobs: libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0 - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.18.0 + uses: ZedThree/clang-tidy-review/upload@v0.19.0 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index 6c39a93a7ea..c55b3a3d9f5 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.18.0 + - uses: ZedThree/clang-tidy-review/post@v0.19.0 with: lgtm_comment_body: "" num_comments_as_exitcode: false From 8689bdb48144ce2fb5ff22b08b87a0f911330fe7 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 19 May 2024 11:11:51 +0200 Subject: [PATCH 061/105] fix: get rid of duplicate scale events (#5404) --- CHANGELOG.md | 1 + src/widgets/BaseWidget.cpp | 6 ++++- src/widgets/BaseWindow.cpp | 1 - src/widgets/Notebook.cpp | 15 ++++++++++- src/widgets/Notebook.hpp | 5 ++++ src/widgets/dialogs/SettingsDialog.cpp | 4 +-- src/widgets/helper/NotebookTab.cpp | 37 ++++++++++++++------------ src/widgets/helper/NotebookTab.hpp | 10 ++++--- src/widgets/splits/SplitHeader.cpp | 2 ++ 9 files changed, 55 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edba34f08a5..4cc9646e93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) +- Dev: Reduced the amount of scale events. (#5404) ## 2.5.1 diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 5e2c932fd14..564ba5963c6 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -54,7 +54,11 @@ float BaseWidget::scale() const void BaseWidget::setScale(float value) { - // update scale value + if (this->scale_ == value) + { + return; + } + this->scale_ = value; this->scaleChangedEvent(this->scale()); diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index bc8767e4999..f3a2afa151a 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -227,7 +227,6 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) [this]() { postToThread([this] { this->updateScale(); - this->updateScale(); }); }, this->connections_, false); diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 7fdff5495cf..e924329d2a4 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -593,6 +593,12 @@ void Notebook::showTabVisibilityInfoPopup() void Notebook::refresh() { + if (this->refreshPaused_) + { + this->refreshRequested_ = true; + return; + } + this->performLayout(); this->updateTabVisibility(); } @@ -652,13 +658,20 @@ void Notebook::resizeAddButton() this->addButton_->setFixedSize(h, h); } -void Notebook::scaleChangedEvent(float) +void Notebook::scaleChangedEvent(float /*scale*/) { this->resizeAddButton(); + this->refreshPaused_ = true; + this->refreshRequested_ = false; for (auto &i : this->items_) { i.tab->updateSize(); } + this->refreshPaused_ = false; + if (this->refreshRequested_) + { + this->refresh(); + } } void Notebook::resizeEvent(QResizeEvent *) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index ac0162c4283..2829bf4fd3c 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -193,7 +193,12 @@ class Notebook : public BaseWidget bool showAddButton_ = false; int lineOffset_ = 20; bool lockNotebookLayout_ = false; + + bool refreshPaused_ = false; + bool refreshRequested_ = false; + NotebookTabLocation tabLocation_ = NotebookTabLocation::Top; + QAction *lockNotebookLayoutAction_; QAction *showTabsAction_; QAction *toggleTopMostAction_; diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 2a71593eccf..d9ad48fff78 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -397,9 +397,9 @@ void SettingsDialog::refresh() } } -void SettingsDialog::scaleChangedEvent(float newDpi) +void SettingsDialog::scaleChangedEvent(float newScale) { - assert(newDpi == 1.F && + assert(newScale == 1.F && "Scaling is disabled for the settings dialog - its scale should " "always be 1"); diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index f7cb2b3a0f3..cf9141fb700 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -8,7 +8,6 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" -#include "util/Clamp.hpp" #include "util/Helpers.hpp" #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/Notebook.hpp" @@ -25,6 +24,8 @@ #include #include +#include + namespace chatterino { namespace { // Translates the given rectangle by an amount in the direction to appear like the tab is selected. @@ -182,10 +183,15 @@ void NotebookTab::growWidth(int width) } } -int NotebookTab::normalTabWidth() +int NotebookTab::normalTabWidth() const +{ + return this->normalTabWidthForHeight(this->height()); +} + +int NotebookTab::normalTabWidthForHeight(int height) const { float scale = this->scale(); - int width; + int width = 0; QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); @@ -199,13 +205,13 @@ int NotebookTab::normalTabWidth() width = (metrics.horizontalAdvance(this->getTitle()) + int(16 * scale)); } - if (this->height() > 150 * scale) + if (static_cast(height) > 150 * scale) { - width = this->height(); + width = height; } else { - width = clamp(width, this->height(), int(150 * scale)); + width = std::clamp(width, height, static_cast(150 * scale)); } return width; @@ -214,8 +220,8 @@ int NotebookTab::normalTabWidth() void NotebookTab::updateSize() { float scale = this->scale(); - int width = this->normalTabWidth(); - auto height = int(NOTEBOOK_TAB_HEIGHT * scale); + auto height = static_cast(NOTEBOOK_TAB_HEIGHT * scale); + int width = this->normalTabWidthForHeight(height); if (width < this->growWidth_) { @@ -628,13 +634,13 @@ void NotebookTab::paintEvent(QPaintEvent *) } } -bool NotebookTab::hasXButton() +bool NotebookTab::hasXButton() const { return getSettings()->showTabCloseButton && this->notebook_->getAllowUserTabManagement(); } -bool NotebookTab::shouldDrawXButton() +bool NotebookTab::shouldDrawXButton() const { return this->hasXButton() && (this->mouseOver_ || this->selected_); } @@ -820,18 +826,15 @@ void NotebookTab::update() Button::update(); } -QRect NotebookTab::getXRect() +QRect NotebookTab::getXRect() const { QRect rect = this->rect(); float s = this->scale(); int size = static_cast(16 * s); - int centerAdjustment = - this->tabLocation_ == - (NotebookTabLocation::Top || - this->tabLocation_ == NotebookTabLocation::Bottom) - ? (size / 3) // slightly off true center - : (size / 2); // true center + int centerAdjustment = this->tabLocation_ == NotebookTabLocation::Top + ? (size / 3) // slightly off true center + : (size / 2); // true center QRect xRect(rect.right() - static_cast(20 * s), rect.center().y() - centerAdjustment, size, size); diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 65b1f46ed31..6dc7d9464cf 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -71,7 +71,7 @@ class NotebookTab : public Button void hideTabXChanged(); void growWidth(int width); - int normalTabWidth(); + int normalTabWidth() const; protected: void themeChangedEvent() override; @@ -100,11 +100,13 @@ class NotebookTab : public Button private: void showRenameDialog(); - bool hasXButton(); - bool shouldDrawXButton(); - QRect getXRect(); + bool hasXButton() const; + bool shouldDrawXButton() const; + QRect getXRect() const; void titleUpdated(); + int normalTabWidthForHeight(int height) const; + QPropertyAnimation positionChangedAnimation_; bool positionChangedAnimationRunning_ = false; QPoint positionAnimationDesiredPoint_; diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 6b7021798ea..35fb1dbde40 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -271,6 +271,8 @@ SplitHeader::SplitHeader(Split *split) } }); } + + this->scaleChangedEvent(this->scale()); } void SplitHeader::initializeLayout() From 8ba570415bb9c61f8fe1b213bbbf7150ea6fc533 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 19 May 2024 22:04:04 +0200 Subject: [PATCH 062/105] fix: usercard jumping when loading data (#5406) --- CHANGELOG.md | 2 +- src/widgets/Label.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc9646e93f..7fb4f7e5781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) -- Dev: Reduced the amount of scale events. (#5404) +- Dev: Reduced the amount of scale events. (#5404, #5406) ## 2.5.1 diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index 1d3e7067f6c..529fb75751d 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -20,6 +20,7 @@ Label::Label(BaseWidget *parent, QString text, FontStyle style) [this] { this->updateSize(); }); + this->updateSize(); } const QString &Label::getText() const From da526b379b65a2d9fe862dc407d71ff66b6685a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 22:10:45 +0200 Subject: [PATCH 063/105] chore(deps): bump jurplel/install-qt-action from 3.3.0 to 4.0.0 (#5408) * chore(deps): bump jurplel/install-qt-action from 3.3.0 to 4.0.0 Bumps [jurplel/install-qt-action](https://github.com/jurplel/install-qt-action) from 3.3.0 to 4.0.0. - [Release notes](https://github.com/jurplel/install-qt-action/releases) - [Commits](https://github.com/jurplel/install-qt-action/compare/v3.3.0...v4.0.0) --- updated-dependencies: - dependency-name: jurplel/install-qt-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Fix CI (#5410) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wissididom <30803034+Wissididom@users.noreply.github.com> --- .github/workflows/build.yml | 22 +++++++++++----------- .github/workflows/clang-tidy.yml | 2 +- .github/workflows/test-macos.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a4d077174d..48433029c2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,15 +150,24 @@ jobs: - name: Install Qt5 if: startsWith(matrix.qt-version, '5.') - uses: jurplel/install-qt-action@v3.3.0 + uses: jurplel/install-qt-action@v4.0.0 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 version: ${{ matrix.qt-version }} + - name: Install Qt6 + if: startsWith(matrix.qt-version, '6.') + uses: jurplel/install-qt-action@v4.0.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: qt5compat qtimageformats + 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 + uses: jurplel/install-qt-action@v4.0.0 with: cache: false modules: qtimageformats @@ -176,15 +185,6 @@ jobs: cd plugins/imageformats echo "PLUGIN_PATH=$(pwd)" | Out-File -Path "$Env:GITHUB_OUTPUT" -Encoding ASCII - - 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 }} - # WINDOWS - name: Enable Developer Command Prompt (Windows) if: startsWith(matrix.os, 'windows') diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 785fb593242..19052648b5e 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -29,7 +29,7 @@ jobs: - name: Install Qt6 if: startsWith(matrix.qt-version, '6.') - uses: jurplel/install-qt-action@v3.3.0 + uses: jurplel/install-qt-action@v4.0.0 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index d606c870880..b22766e0961 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 # allows for tags access - name: Install Qt - uses: jurplel/install-qt-action@v3.3.0 + uses: jurplel/install-qt-action@v4.0.0 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0b73ee84f30..8038d1aca3a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -57,7 +57,7 @@ jobs: fetch-depth: 0 # allows for tags access - name: Install Qt - uses: jurplel/install-qt-action@v3.3.0 + uses: jurplel/install-qt-action@v4.0.0 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 From ec6b1ef24d517c9fe6f38c910b8e36c21ec38b4c Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 22 May 2024 15:23:33 +0200 Subject: [PATCH 064/105] fix(luals-meta): generate globals in `c2` and echo @lua commands (#5385) Co-authored-by: Mm2PL --- CHANGELOG.md | 1 + docs/plugin-meta.lua | 58 +++++++++++----------- scripts/make_luals_meta.py | 43 +++++++++------- src/common/Channel.hpp | 2 +- src/controllers/plugins/LuaAPI.hpp | 2 +- src/controllers/plugins/api/ChannelRef.hpp | 44 ++++++++-------- 6 files changed, 78 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb4f7e5781..2c85db1b3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) - Dev: Reduced the amount of scale events. (#5404, #5406) +- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) ## 2.5.1 diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 7b72b46d5f3..391b5745d42 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -16,7 +16,7 @@ c2.EventType = {} ---@class CommandContext ---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. ----@field channel Channel The channel the command was executed in. +---@field channel c2.Channel The channel the command was executed in. ---@class CompletionList ---@field values string[] The completions @@ -30,98 +30,98 @@ c2.EventType = {} -- Begin src/common/Channel.hpp ----@alias ChannelType integer ----@type { None: ChannelType, Direct: ChannelType, Twitch: ChannelType, TwitchWhispers: ChannelType, TwitchWatching: ChannelType, TwitchMentions: ChannelType, TwitchLive: ChannelType, TwitchAutomod: ChannelType, TwitchEnd: ChannelType, Irc: ChannelType, Misc: ChannelType } -ChannelType = {} +---@alias c2.ChannelType integer +---@type { None: c2.ChannelType, Direct: c2.ChannelType, Twitch: c2.ChannelType, TwitchWhispers: c2.ChannelType, TwitchWatching: c2.ChannelType, TwitchMentions: c2.ChannelType, TwitchLive: c2.ChannelType, TwitchAutomod: c2.ChannelType, TwitchEnd: c2.ChannelType, Irc: c2.ChannelType, Misc: c2.ChannelType } +c2.ChannelType = {} -- End src/common/Channel.hpp -- Begin src/controllers/plugins/api/ChannelRef.hpp ----@alias Platform integer +---@alias c2.Platform integer --- This enum describes a platform for the purpose of searching for a channel. --- Currently only Twitch is supported because identifying IRC channels is tricky. ----@type { Twitch: Platform } -Platform = {} +---@type { Twitch: c2.Platform } +c2.Platform = {} ----@class Channel -Channel = {} +---@class c2.Channel +c2.Channel = {} --- Returns true if the channel this object points to is valid. --- If the object expired, returns false --- If given a non-Channel object, it errors. --- ---@return boolean success -function Channel:is_valid() end +function c2.Channel:is_valid() end --- Gets the channel's name. This is the lowercase login name. --- ---@return string name -function Channel:get_name() end +function c2.Channel:get_name() end --- Gets the channel's type --- ----@return ChannelType -function Channel:get_type() end +---@return c2.ChannelType +function c2.Channel:get_type() end --- Get the channel owner's display name. This may contain non-lowercase ascii characters. --- ---@return string name -function Channel:get_display_name() end +function c2.Channel:get_display_name() end --- Sends a message to the target channel. --- Note that this does not execute client-commands. --- ---@param message string ---@param execute_commands boolean Should commands be run on the text? -function Channel:send_message(message, execute_commands) end +function c2.Channel:send_message(message, execute_commands) end --- Adds a system message client-side --- ---@param message string -function Channel:add_system_message(message) end +function c2.Channel:add_system_message(message) end --- Returns true for twitch channels. --- Compares the channel Type. Note that enum values aren't guaranteed, just --- that they are equal to the exposed enum. --- ---@return boolean -function Channel:is_twitch_channel() end +function c2.Channel:is_twitch_channel() end --- Returns a copy of the channel mode settings (subscriber only, r9k etc.) --- ---@return RoomModes -function Channel:get_room_modes() end +function c2.Channel:get_room_modes() end --- Returns a copy of the stream status. --- ---@return StreamStatus -function Channel:get_stream_status() end +function c2.Channel:get_stream_status() end --- Returns the Twitch user ID of the owner of the channel. --- ---@return string -function Channel:get_twitch_id() end +function c2.Channel:get_twitch_id() end --- Returns true if the channel is a Twitch channel and the user owns it --- ---@return boolean -function Channel:is_broadcaster() end +function c2.Channel:is_broadcaster() end --- Returns true if the channel is a Twitch channel and the user is a moderator in the channel --- Returns false for broadcaster. --- ---@return boolean -function Channel:is_mod() end +function c2.Channel:is_mod() end --- Returns true if the channel is a Twitch channel and the user is a VIP in the channel --- Returns false for broadcaster. --- ---@return boolean -function Channel:is_vip() end +function c2.Channel:is_vip() end ---@return string -function Channel:__tostring() end +function c2.Channel:__tostring() end --- Finds a channel by name. --- Misc channels are marked as Twitch: @@ -132,15 +132,15 @@ function Channel:__tostring() end --- - /automod --- ---@param name string Which channel are you looking for? ----@param platform Platform Where to search for the channel? ----@return Channel? -function Channel.by_name(name, platform) end +---@param platform c2.Platform Where to search for the channel? +---@return c2.Channel? +function c2.Channel.by_name(name, platform) end --- Finds a channel by the Twitch user ID of its owner. --- ---@param id string ID of the owner of the channel. ----@return Channel? -function Channel.by_twitch_id(id) end +---@return c2.Channel? +function c2.Channel.by_twitch_id(id) end ---@class RoomModes ---@field unique_chat boolean You might know this as r9kbeta or robot9000. diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index 58a06242806..adbbeb9dfac 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -134,11 +134,13 @@ def next_doc_comment(self) -> Optional[list[str]]: def read_class_body(self) -> list[list[str]]: """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" items = [] + nesting = -1 # for the opening brace while (line := self.peek_line()) is not None: - if line.startswith("};"): + if line.startswith("};") and nesting == 0: self.next_line() break if not is_comment_start(line): + nesting += line.count("{") - line.count("}") self.next_line() continue doc = self.next_doc_comment() @@ -231,21 +233,6 @@ def read_file(path: Path, out: TextIOWrapper): else: header = doc_comment - # include block - if header[0].startswith("@includefile "): - for comment in header: - if not comment.startswith("@includefile "): - panic( - path, - reader.line_no(), - f"Invalid include block - got line '{comment}'", - ) - filename = comment.split(" ", 1)[1] - out.write(f"-- Begin src/{filename}\n\n") - read_file(repo_root / "src" / filename, out) - out.write(f"-- End src/{filename}\n\n") - continue - # enum if header[0].startswith("@exposeenum "): if len(header) > 1: @@ -270,7 +257,7 @@ def read_file(path: Path, out: TextIOWrapper): continue # class - if header[0].startswith("@lua@class "): + elif header[0].startswith("@lua@class "): name = header[0].split(" ", 1)[1] classname = name.split(":")[0].strip() printmsg(path, reader.line_no(), f"class {classname}") @@ -311,11 +298,29 @@ def read_file(path: Path, out: TextIOWrapper): for func in funcs: write_func(path, reader.line_no(), func, out) continue - # global function - if header[-1].startswith("@exposed "): + elif header[-1].startswith("@exposed "): write_func(path, reader.line_no(), doc_comment, out) continue + else: + for comment in header: + inline_command(path, reader.line_no(), comment, out) + + +def inline_command(path: Path, line: int, comment: str, out: TextIOWrapper): + if comment.startswith("@includefile "): + filename = comment.split(" ", 1)[1] + out.write(f"-- Begin src/{filename}\n\n") + read_file(repo_root / "src" / filename, out) + out.write(f"-- End src/{filename}\n\n") + elif comment.startswith("@lua@class"): + panic( + path, + line, + "Unexpected @lua@class command. @lua@class must be placed at the start of the comment block!", + ) + elif comment.startswith("@lua@"): + out.write(f'---{comment.replace("@lua", "", 1)}\n') if __name__ == "__main__": diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 6adac6a76f4..601142b4a86 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -32,7 +32,7 @@ class Channel : public std::enable_shared_from_this public: // This is for Lua. See scripts/make_luals_meta.py /** - * @exposeenum ChannelType + * @exposeenum c2.ChannelType */ enum class Type { None, diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 15be99c6fa8..fd2c12e4144 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -39,7 +39,7 @@ enum class EventType { /** * @lua@class CommandContext * @lua@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. - * @lua@field channel Channel The channel the command was executed in. + * @lua@field channel c2.Channel The channel the command was executed in. */ /** diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index abc6b421f6f..32e1946abcb 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -13,7 +13,7 @@ namespace chatterino::lua::api { /** * This enum describes a platform for the purpose of searching for a channel. * Currently only Twitch is supported because identifying IRC channels is tricky. - * @exposeenum Platform + * @exposeenum c2.Platform */ enum class LPlatform { Twitch, @@ -21,7 +21,7 @@ enum class LPlatform { }; /** - * @lua@class Channel + * @lua@class c2.Channel */ struct ChannelRef { static void createMetatable(lua_State *L); @@ -49,7 +49,7 @@ struct ChannelRef { * If given a non-Channel object, it errors. * * @lua@return boolean success - * @exposed Channel:is_valid + * @exposed c2.Channel:is_valid */ static int is_valid(lua_State *L); @@ -57,15 +57,15 @@ struct ChannelRef { * Gets the channel's name. This is the lowercase login name. * * @lua@return string name - * @exposed Channel:get_name + * @exposed c2.Channel:get_name */ static int get_name(lua_State *L); /** * Gets the channel's type * - * @lua@return ChannelType - * @exposed Channel:get_type + * @lua@return c2.ChannelType + * @exposed c2.Channel:get_type */ static int get_type(lua_State *L); @@ -73,7 +73,7 @@ struct ChannelRef { * Get the channel owner's display name. This may contain non-lowercase ascii characters. * * @lua@return string name - * @exposed Channel:get_display_name + * @exposed c2.Channel:get_display_name */ static int get_display_name(lua_State *L); @@ -83,7 +83,7 @@ struct ChannelRef { * * @lua@param message string * @lua@param execute_commands boolean Should commands be run on the text? - * @exposed Channel:send_message + * @exposed c2.Channel:send_message */ static int send_message(lua_State *L); @@ -91,7 +91,7 @@ struct ChannelRef { * Adds a system message client-side * * @lua@param message string - * @exposed Channel:add_system_message + * @exposed c2.Channel:add_system_message */ static int add_system_message(lua_State *L); @@ -101,7 +101,7 @@ struct ChannelRef { * that they are equal to the exposed enum. * * @lua@return boolean - * @exposed Channel:is_twitch_channel + * @exposed c2.Channel:is_twitch_channel */ static int is_twitch_channel(lua_State *L); @@ -113,7 +113,7 @@ struct ChannelRef { * Returns a copy of the channel mode settings (subscriber only, r9k etc.) * * @lua@return RoomModes - * @exposed Channel:get_room_modes + * @exposed c2.Channel:get_room_modes */ static int get_room_modes(lua_State *L); @@ -121,7 +121,7 @@ struct ChannelRef { * Returns a copy of the stream status. * * @lua@return StreamStatus - * @exposed Channel:get_stream_status + * @exposed c2.Channel:get_stream_status */ static int get_stream_status(lua_State *L); @@ -129,7 +129,7 @@ struct ChannelRef { * Returns the Twitch user ID of the owner of the channel. * * @lua@return string - * @exposed Channel:get_twitch_id + * @exposed c2.Channel:get_twitch_id */ static int get_twitch_id(lua_State *L); @@ -137,7 +137,7 @@ struct ChannelRef { * Returns true if the channel is a Twitch channel and the user owns it * * @lua@return boolean - * @exposed Channel:is_broadcaster + * @exposed c2.Channel:is_broadcaster */ static int is_broadcaster(lua_State *L); @@ -146,7 +146,7 @@ struct ChannelRef { * Returns false for broadcaster. * * @lua@return boolean - * @exposed Channel:is_mod + * @exposed c2.Channel:is_mod */ static int is_mod(lua_State *L); @@ -155,7 +155,7 @@ struct ChannelRef { * Returns false for broadcaster. * * @lua@return boolean - * @exposed Channel:is_vip + * @exposed c2.Channel:is_vip */ static int is_vip(lua_State *L); @@ -165,7 +165,7 @@ struct ChannelRef { /** * @lua@return string - * @exposed Channel:__tostring + * @exposed c2.Channel:__tostring */ static int to_string(lua_State *L); @@ -184,9 +184,9 @@ struct ChannelRef { * - /automod * * @lua@param name string Which channel are you looking for? - * @lua@param platform Platform Where to search for the channel? - * @lua@return Channel? - * @exposed Channel.by_name + * @lua@param platform c2.Platform Where to search for the channel? + * @lua@return c2.Channel? + * @exposed c2.Channel.by_name */ static int get_by_name(lua_State *L); @@ -194,8 +194,8 @@ struct ChannelRef { * Finds a channel by the Twitch user ID of its owner. * * @lua@param id string ID of the owner of the channel. - * @lua@return Channel? - * @exposed Channel.by_twitch_id + * @lua@return c2.Channel? + * @exposed c2.Channel.by_twitch_id */ static int get_by_twitch_id(lua_State *L); }; From 491b6db72f9903e56829ae77b6846b35a50e219d Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 25 May 2024 13:12:04 +0200 Subject: [PATCH 065/105] chore: remove unused timegates (#5361) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 15 +--- src/singletons/Settings.hpp | 36 --------- src/widgets/settingspages/GeneralPage.cpp | 90 ---------------------- 4 files changed, 3 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c85db1b3f8..93dcf2ea653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) - Dev: Reduced the amount of scale events. (#5404, #5406) +- Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) ## 2.5.1 diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 21a52ef1de9..9dd2d7a4b70 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -435,19 +435,8 @@ std::vector parseNoticeMessage(Communi::IrcNoticeMessage *message) // default case std::vector builtMessages; - auto content = message->content(); - if (content.startsWith( - "Your settings prevent you from sending this whisper", - Qt::CaseInsensitive) && - getSettings()->helixTimegateWhisper.getValue() == - HelixTimegateOverride::Timegate) - { - content = content + - " Consider setting \"Helix timegate /w behaviour\" " - "to \"Always use Helix\" in your Chatterino settings."; - } - builtMessages.emplace_back( - makeSystemMessage(content, calculateMessageTime(message).time())); + builtMessages.emplace_back(makeSystemMessage( + message->content(), calculateMessageTime(message).time())); return builtMessages; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 660b3c143d6..f0c19edf549 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -47,19 +47,6 @@ enum UsernameDisplayMode : int { UsernameAndLocalizedName = 3, // Username (Localized name) }; -enum HelixTimegateOverride : int { - // Use the default timegated behaviour - // This means we use the old IRC command up until the migration date and - // switch over to the Helix API only after the migration date - Timegate = 1, - - // Ignore timegating and always force use the IRC command - AlwaysUseIRC = 2, - - // Ignore timegating and always force use the Helix API - AlwaysUseHelix = 3, -}; - enum ThumbnailPreviewMode : int { DontShow = 0, @@ -538,29 +525,6 @@ class Settings 1000, }; - // Temporary time-gate-overrides - EnumSetting helixTimegateRaid = { - "/misc/twitch/helix-timegate/raid", - HelixTimegateOverride::Timegate, - }; - EnumSetting helixTimegateWhisper = { - "/misc/twitch/helix-timegate/whisper", - HelixTimegateOverride::Timegate, - }; - EnumSetting helixTimegateVIPs = { - "/misc/twitch/helix-timegate/vips", - HelixTimegateOverride::Timegate, - }; - EnumSetting helixTimegateModerators = { - "/misc/twitch/helix-timegate/moderators", - HelixTimegateOverride::Timegate, - }; - - EnumSetting helixTimegateCommercial = { - "/misc/twitch/helix-timegate/commercial", - HelixTimegateOverride::Timegate, - }; - EnumStringSetting chatSendProtocol = { "/misc/chatSendProtocol", ChatSendProtocol::Default}; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index d129e46eb71..e21e9809e8a 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -1161,96 +1161,6 @@ void GeneralPage::initLayout(GeneralPageView &layout) "@mention for the related thread. If the reply context is hidden, " "these mentions will never be stripped."); - // Helix timegate settings - auto helixTimegateGetValue = [](auto val) { - switch (val) - { - case HelixTimegateOverride::Timegate: - return "Timegate"; - case HelixTimegateOverride::AlwaysUseIRC: - return "Always use IRC"; - case HelixTimegateOverride::AlwaysUseHelix: - return "Always use Helix"; - default: - return "Timegate"; - } - }; - - auto helixTimegateSetValue = [](auto args) { - const auto &v = args.value; - if (v == "Timegate") - { - return HelixTimegateOverride::Timegate; - } - if (v == "Always use IRC") - { - return HelixTimegateOverride::AlwaysUseIRC; - } - if (v == "Always use Helix") - { - return HelixTimegateOverride::AlwaysUseHelix; - } - - qCDebug(chatterinoSettings) << "Unknown Helix timegate override value" - << v << ", using default value Timegate"; - return HelixTimegateOverride::Timegate; - }; - - auto *helixTimegateRaid = - layout.addDropdown::type>( - "Helix timegate /raid behaviour", - {"Timegate", "Always use IRC", "Always use Helix"}, - s.helixTimegateRaid, - helixTimegateGetValue, // - helixTimegateSetValue, // - false); - helixTimegateRaid->setMinimumWidth( - helixTimegateRaid->minimumSizeHint().width()); - - auto *helixTimegateWhisper = - layout.addDropdown::type>( - "Helix timegate /w behaviour", - {"Timegate", "Always use IRC", "Always use Helix"}, - s.helixTimegateWhisper, - helixTimegateGetValue, // - helixTimegateSetValue, // - false); - helixTimegateWhisper->setMinimumWidth( - helixTimegateWhisper->minimumSizeHint().width()); - - auto *helixTimegateVIPs = - layout.addDropdown::type>( - "Helix timegate /vips behaviour", - {"Timegate", "Always use IRC", "Always use Helix"}, - s.helixTimegateVIPs, - helixTimegateGetValue, // - helixTimegateSetValue, // - false); - helixTimegateVIPs->setMinimumWidth( - helixTimegateVIPs->minimumSizeHint().width()); - - auto *helixTimegateCommercial = - layout.addDropdown::type>( - "Helix timegate /commercial behaviour", - {"Timegate", "Always use IRC", "Always use Helix"}, - s.helixTimegateCommercial, - helixTimegateGetValue, // - helixTimegateSetValue, // - false); - helixTimegateCommercial->setMinimumWidth( - helixTimegateCommercial->minimumSizeHint().width()); - - auto *helixTimegateModerators = - layout.addDropdown::type>( - "Helix timegate /mods behaviour", - {"Timegate", "Always use IRC", "Always use Helix"}, - s.helixTimegateModerators, - helixTimegateGetValue, // - helixTimegateSetValue, // - false); - helixTimegateModerators->setMinimumWidth( - helixTimegateModerators->minimumSizeHint().width()); - layout.addDropdownEnumClass( "Chat send protocol", qmagicenum::enumNames(), s.chatSendProtocol, From c5802a0f4918bbf1e0e6741315ada8a5ae1131e0 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 25 May 2024 13:53:37 +0200 Subject: [PATCH 066/105] fix: use `irc-unknown` when irc server couldn't be loaded (#5419) --- CHANGELOG.md | 1 + src/common/Channel.cpp | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93dcf2ea653..3f12f82a400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Use Qt's high DPI scaling. (#4868, #5400) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 872ba246209..7141dbe7efc 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -92,8 +92,16 @@ void Channel::addMessage(MessagePtr message, auto *irc = dynamic_cast(this); if (irc != nullptr) { - channelPlatform = QString("irc-%1").arg( - irc->server()->userFriendlyIdentifier()); + auto *ircServer = irc->server(); + if (ircServer != nullptr) + { + channelPlatform = QString("irc-%1").arg( + irc->server()->userFriendlyIdentifier()); + } + else + { + channelPlatform = "irc-unknown"; + } } } else if (this->isTwitchChannel()) From d161036b18a000e7c161719795fad8c426df85ac Mon Sep 17 00:00:00 2001 From: KleberPF <43550602+KleberPF@users.noreply.github.com> Date: Sat, 25 May 2024 09:39:19 -0300 Subject: [PATCH 067/105] Add feature to duplicate tabs (#5277) Co-authored-by: Rasmus Karlsson --- .clang-tidy | 1 + CHANGELOG.md | 1 + src/common/Channel.hpp | 32 ++++++++++++++- src/widgets/Notebook.cpp | 59 ++++++++++++++++++++++++++- src/widgets/Notebook.hpp | 9 ++++ src/widgets/helper/NotebookTab.cpp | 4 ++ src/widgets/splits/SplitContainer.cpp | 53 +++++++++++++++++++++++- src/widgets/splits/SplitContainer.hpp | 2 + 8 files changed, 158 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 170ad019a41..ae07280120b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -22,6 +22,7 @@ Checks: "-*, -readability-magic-numbers, -performance-noexcept-move-constructor, -misc-non-private-member-variables-in-classes, + -misc-no-recursion, -cppcoreguidelines-non-private-member-variables-in-classes, -modernize-use-nodiscard, -modernize-use-trailing-return-type, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f12f82a400..f546e8a2ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) +- Minor: Added the ability to duplicate tabs. (#5277) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Use Qt's high DPI scaling. (#4868, #5400) diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 601142b4a86..106d6cabc8f 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -4,6 +4,7 @@ #include "controllers/completion/TabCompletionModel.hpp" #include "messages/LimitedQueue.hpp" +#include #include #include #include @@ -45,7 +46,7 @@ class Channel : public std::enable_shared_from_this TwitchAutomod, TwitchEnd, Irc, - Misc + Misc, }; explicit Channel(const QString &name, Type type); @@ -151,3 +152,32 @@ class IndirectChannel }; } // namespace chatterino + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::Channel::Type value) noexcept +{ + using Type = chatterino::Channel::Type; + switch (value) + { + case Type::Twitch: + return "twitch"; + case Type::TwitchWhispers: + return "whispers"; + case Type::TwitchWatching: + return "watching"; + case Type::TwitchMentions: + return "mentions"; + case Type::TwitchLive: + return "live"; + case Type::TwitchAutomod: + return "automod"; + case Type::Irc: + return "irc"; + case Type::Misc: + return "misc"; + default: + return default_tag; + } +} diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index e924329d2a4..a3871eee90f 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -29,6 +29,8 @@ #include #include +#include + namespace chatterino { Notebook::Notebook(QWidget *parent) @@ -87,6 +89,12 @@ Notebook::Notebook(QWidget *parent) } NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) +{ + return this->addPageAt(page, -1, std::move(title), select); +} + +NotebookTab *Notebook::addPageAt(QWidget *page, int position, QString title, + bool select) { // Queue up save because: Tab added getIApp()->getWindows()->queueSave(); @@ -101,7 +109,14 @@ NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) item.page = page; item.tab = tab; - this->items_.append(item); + if (position == -1) + { + this->items_.push_back(item); + } + else + { + this->items_.insert(position, item); + } page->hide(); page->setParent(this); @@ -165,6 +180,48 @@ void Notebook::removePage(QWidget *page) this->performLayout(true); } +void Notebook::duplicatePage(QWidget *page) +{ + auto *item = this->findItem(page); + assert(item != nullptr); + if (item == nullptr) + { + return; + } + + auto *container = dynamic_cast(item->page); + if (!container) + { + return; + } + + auto *newContainer = new SplitContainer(this); + if (!container->getSplits().empty()) + { + auto descriptor = container->buildDescriptor(); + newContainer->applyFromDescriptor(descriptor); + } + + const auto tabPosition = this->indexOf(page); + auto newTabPosition = -1; + if (tabPosition != -1) + { + newTabPosition = tabPosition + 1; + } + auto newTabHighlightState = item->tab->highlightState(); + QString newTabTitle = ""; + if (item->tab->hasCustomTitle()) + { + newTabTitle = item->tab->getCustomTitle(); + } + + auto *tab = + this->addPageAt(newContainer, newTabPosition, newTabTitle, false); + tab->setHighlightState(newTabHighlightState); + + newContainer->setTab(tab); +} + void Notebook::removeCurrentPage() { if (this->selectedPage_ != nullptr) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 2829bf4fd3c..c024998d881 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -42,7 +42,16 @@ class Notebook : public BaseWidget NotebookTab *addPage(QWidget *page, QString title = QString(), bool select = false); + + /** + * @brief Adds a page to the Notebook at a given position. + * + * @param position if set to -1, adds the page to the end + **/ + NotebookTab *addPageAt(QWidget *page, int position, + QString title = QString(), bool select = false); void removePage(QWidget *page); + void duplicatePage(QWidget *page); void removeCurrentPage(); /** diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index cf9141fb700..1084abca74e 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -99,6 +99,10 @@ NotebookTab::NotebookTab(Notebook *notebook) getIApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Window, "popup", {{"window"}})); + this->menu_.addAction("Duplicate Tab", [this]() { + this->notebook_->duplicatePage(this->page); + }); + highlightNewMessagesAction_ = new QAction("Mark Tab as Unread on New Messages", &this->menu_); highlightNewMessagesAction_->setCheckable(true); diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 2a274c6cc82..bee31cd116d 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -5,9 +5,12 @@ #include "common/QLogging.hpp" #include "common/WindowDescriptors.hpp" #include "debug/AssertInGuiThread.hpp" +#include "providers/irc/IrcChannel2.hpp" +#include "providers/irc/IrcServer.hpp" #include "singletons/Fonts.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "util/QMagicEnum.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/Notebook.hpp" @@ -762,6 +765,11 @@ SplitContainer::Node *SplitContainer::getBaseNode() return &this->baseNode_; } +NodeDescriptor SplitContainer::buildDescriptor() const +{ + return this->buildDescriptorRecursively(&this->baseNode_); +} + void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) { assert(this->baseNode_.type_ == Node::Type::EmptyRoot); @@ -799,6 +807,49 @@ void SplitContainer::popup() window.show(); } +NodeDescriptor SplitContainer::buildDescriptorRecursively( + const Node *currentNode) const +{ + if (currentNode->children_.empty()) + { + const auto channelType = + currentNode->split_->getIndirectChannel().getType(); + + SplitNodeDescriptor result; + result.type_ = qmagicenum::enumNameString(channelType); + + switch (channelType) + { + case Channel::Type::Irc: { + if (auto *ircChannel = dynamic_cast( + currentNode->split_->getChannel().get())) + { + if (ircChannel->server()) + { + result.server_ = ircChannel->server()->id(); + } + } + } + break; + } + + result.channelName_ = currentNode->split_->getChannel()->getName(); + result.filters_ = currentNode->split_->getFilters(); + return result; + } + + ContainerNodeDescriptor descriptor; + for (const auto &child : currentNode->children_) + { + descriptor.vertical_ = + currentNode->type_ == Node::Type::VerticalContainer; + descriptor.items_.push_back( + this->buildDescriptorRecursively(child.get())); + } + + return descriptor; +} + void SplitContainer::applyFromDescriptorRecursively( const NodeDescriptor &rootNode, Node *baseNode) { @@ -849,9 +900,9 @@ void SplitContainer::applyFromDescriptorRecursively( } const auto &splitNode = *inner; auto *split = new Split(this); + split->setFilters(splitNode.filters_); split->setChannel(WindowManager::decodeChannel(splitNode)); split->setModerationMode(splitNode.moderationMode_); - split->setFilters(splitNode.filters_); auto *node = new Node(); node->parent_ = baseNode; diff --git a/src/widgets/splits/SplitContainer.hpp b/src/widgets/splits/SplitContainer.hpp index 9022085dad8..9e4a6cc7515 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -220,6 +220,7 @@ class SplitContainer final : public BaseWidget void hideResizeHandles(); void resetMouseStatus(); + NodeDescriptor buildDescriptor() const; void applyFromDescriptor(const NodeDescriptor &rootNode); void popup(); @@ -237,6 +238,7 @@ class SplitContainer final : public BaseWidget void resizeEvent(QResizeEvent *event) override; private: + NodeDescriptor buildDescriptorRecursively(const Node *currentNode) const; void applyFromDescriptorRecursively(const NodeDescriptor &rootNode, Node *baseNode); From be7b517499e973396d02f10d84bf4272eaeb452d Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 25 May 2024 18:26:38 +0200 Subject: [PATCH 068/105] Update Windows Qt6 version from 6.5.0 to 6.7.1 (#5420) --- .github/workflows/build.yml | 36 ++++---------------------- .github/workflows/create-installer.yml | 2 +- .github/workflows/test-windows.yml | 2 +- CHANGELOG.md | 1 + 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48433029c2d..c45b0165beb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,7 +120,7 @@ jobs: skip-crashpad: false # Windows - os: windows-latest - qt-version: 6.5.0 + qt-version: 6.7.1 force-lto: false plugins: true skip-artifact: false @@ -165,26 +165,6 @@ jobs: modules: qt5compat qtimageformats version: ${{ matrix.qt-version }} - - name: Install Qt 6.5.3 imageformats - if: startsWith(matrix.qt-version, '6.') - uses: jurplel/install-qt-action@v4.0.0 - with: - cache: false - modules: qtimageformats - set-env: false - version: 6.5.3 - extra: --noarchives - - - name: Find Qt 6.5.3 Path - if: startsWith(matrix.qt-version, '6.') && startsWith(matrix.os, 'windows') - shell: pwsh - id: find-good-imageformats - run: | - cd "$Env:RUNNER_WORKSPACE/Qt/6.5.3" - cd (Get-ChildItem)[0].Name - cd plugins/imageformats - echo "PLUGIN_PATH=$(pwd)" | Out-File -Path "$Env:GITHUB_OUTPUT" -Encoding ASCII - # WINDOWS - name: Enable Developer Command Prompt (Windows) if: startsWith(matrix.os, 'windows') @@ -273,12 +253,6 @@ jobs: cp bin/chatterino.exe Chatterino2/ echo nightly > Chatterino2/modes - - name: Fix Qt6 (windows) - if: startsWith(matrix.qt-version, '6.') && startsWith(matrix.os, 'windows') - working-directory: build - run: | - cp ${{ steps.find-good-imageformats.outputs.PLUGIN_PATH }}/qwebp.dll Chatterino2/imageformats/qwebp.dll - - name: Package (windows) if: startsWith(matrix.os, 'windows') working-directory: build @@ -360,15 +334,15 @@ jobs: # Windows - uses: actions/download-artifact@v4 - name: Windows Qt6.5.0 + name: Windows Qt6.7.1 with: - name: chatterino-windows-x86-64-Qt-6.5.0.zip + name: chatterino-windows-x86-64-Qt-6.7.1.zip path: release-artifacts/ - uses: actions/download-artifact@v4 - name: Windows Qt6.5.0 symbols + name: Windows Qt6.7.1 symbols with: - name: chatterino-windows-x86-64-Qt-6.5.0-symbols.pdb.7z + name: chatterino-windows-x86-64-Qt-6.7.1-symbols.pdb.7z path: release-artifacts/ - uses: actions/download-artifact@v4 diff --git a/.github/workflows/create-installer.yml b/.github/workflows/create-installer.yml index 626d3e50363..ac060c96d3a 100644 --- a/.github/workflows/create-installer.yml +++ b/.github/workflows/create-installer.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} strategy: matrix: - qt-version: ["6.5.0"] + qt-version: ["6.7.1"] steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8038d1aca3a..ac1f1c9a079 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [windows-latest] - qt-version: [5.15.2, 6.5.0] + qt-version: [5.15.2, 6.7.1] plugins: [false] skip-artifact: [false] skip-crashpad: [false] diff --git a/CHANGELOG.md b/CHANGELOG.md index f546e8a2ef0..206abd69ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Minor: Added the ability to duplicate tabs. (#5277) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) +- Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Use Qt's high DPI scaling. (#4868, #5400) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) From 49bdeb3b496f58ab5593a3f155700c5a3455ed9f Mon Sep 17 00:00:00 2001 From: kornes <28986062+kornes@users.noreply.github.com> Date: Sat, 25 May 2024 17:17:37 +0000 Subject: [PATCH 069/105] chore: update vcpkg baseline (Qt 6.7.0) (#5422) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + scripts/get-vcpkg-package-versions.sh | 11 +++++++++++ vcpkg.json | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100755 scripts/get-vcpkg-package-versions.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 206abd69ef1..5f5c324edf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) +- Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Use Qt's high DPI scaling. (#4868, #5400) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/scripts/get-vcpkg-package-versions.sh b/scripts/get-vcpkg-package-versions.sh new file mode 100755 index 00000000000..f726a6cbb1a --- /dev/null +++ b/scripts/get-vcpkg-package-versions.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +dependencies="$(jq -r -c '.dependencies[] | if type == "string" then . else .name end' vcpkg.json)" +dependencies+=" openssl" +baseline="$(jq -r -c '."builtin-baseline"' vcpkg.json)" + +for dependency_name in $dependencies; do + dependency_url="https://raw.githubusercontent.com/microsoft/vcpkg/${baseline}/ports/${dependency_name}/vcpkg.json" + dependency_version="$(curl -s "$dependency_url" | jq -rc '.version')" + echo "Dependency $dependency_name is at version '$dependency_version' in baseline $baseline" +done diff --git a/vcpkg.json b/vcpkg.json index ed80080e548..5ab85f87a8e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "chatterino", "version": "2.0.0", - "builtin-baseline": "c6d6efed3e9b4242765bfe1b5c5befffd85f7b92", + "builtin-baseline": "01f602195983451bc83e72f4214af2cbc495aa94", "dependencies": [ "boost-asio", "boost-circular-buffer", From 9ec1022405539b1a9d36870dfd645397e6ce4edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 01:37:05 +0000 Subject: [PATCH 070/105] chore(deps): bump lib/settings from `03e8af1` to `80b8cb6` (#5427) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index 03e8af1934e..80b8cb605a8 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 03e8af1934e6151edfe8a44dfb025b747a31acdf +Subproject commit 80b8cb605a82134c2ce5abf56715c3443413e947 From c3bb99eb01001a01da7575b28d40ad0a46fd67e1 Mon Sep 17 00:00:00 2001 From: kornes <28986062+kornes@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:12:48 +0000 Subject: [PATCH 071/105] Fix: tabs move animation for duplicated tabs (#5426) * fix: check endValue for running animations only * exit early when move is not needed * ref: remove useless `positionChangedAnimationRunning_` * check for parent notebook visibility instead * ref: rename `pos` param to `targetPos` --- CHANGELOG.md | 1 + src/widgets/helper/NotebookTab.cpp | 23 +++++++++++++---------- src/widgets/helper/NotebookTab.hpp | 3 +-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5c324edf9..15e552c4e58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Minor: Added the ability to duplicate tabs. (#5277) +- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index 1084abca74e..a5d8cfb690e 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -15,6 +15,7 @@ #include "widgets/splits/SplitContainer.hpp" #include +#include #include #include #include @@ -407,22 +408,24 @@ void NotebookTab::hideTabXChanged() this->update(); } -void NotebookTab::moveAnimated(QPoint pos, bool animated) +void NotebookTab::moveAnimated(QPoint targetPos, bool animated) { - this->positionAnimationDesiredPoint_ = pos; + this->positionAnimationDesiredPoint_ = targetPos; - QWidget *w = this->window(); - - if ((w != nullptr && !w->isVisible()) || !animated || - !this->positionChangedAnimationRunning_) + if (this->pos() == targetPos) { - this->move(pos); + return; + } - this->positionChangedAnimationRunning_ = true; + if (!animated || !this->notebook_->isVisible()) + { + this->move(targetPos); return; } - if (this->positionChangedAnimation_.endValue() == pos) + if (this->positionChangedAnimation_.state() == + QAbstractAnimation::Running && + this->positionChangedAnimation_.endValue() == targetPos) { return; } @@ -430,7 +433,7 @@ void NotebookTab::moveAnimated(QPoint pos, bool animated) this->positionChangedAnimation_.stop(); this->positionChangedAnimation_.setDuration(75); this->positionChangedAnimation_.setStartValue(this->pos()); - this->positionChangedAnimation_.setEndValue(pos); + this->positionChangedAnimation_.setEndValue(targetPos); this->positionChangedAnimation_.start(); } diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 6dc7d9464cf..7f746fd7e14 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -65,7 +65,7 @@ class NotebookTab : public Button void setHighlightsEnabled(const bool &newVal); bool hasHighlightsEnabled() const; - void moveAnimated(QPoint pos, bool animated = true); + void moveAnimated(QPoint targetPos, bool animated = true); QRect getDesiredRect() const; void hideTabXChanged(); @@ -108,7 +108,6 @@ class NotebookTab : public Button int normalTabWidthForHeight(int height) const; QPropertyAnimation positionChangedAnimation_; - bool positionChangedAnimationRunning_ = false; QPoint positionAnimationDesiredPoint_; Notebook *notebook_; From 65bfec963b54fd05f42b80810be1f7174233891b Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 1 Jun 2024 12:38:39 +0200 Subject: [PATCH 072/105] feat(emote-popup): save size of popup (#5415) * fix: remove added margins from emote window position * chore: add changelog entry * feat: store size of emote window * chore: update changelog entry * fix: disable layout save * fix: PCH moment * fix: multiply by scale --- CHANGELOG.md | 1 + src/common/WindowDescriptors.cpp | 12 ++++-- src/common/WindowDescriptors.hpp | 2 +- src/singletons/WindowManager.cpp | 38 ++++++++++++------- src/singletons/WindowManager.hpp | 6 +-- src/util/WidgetHelpers.cpp | 60 ++++++++++++++++++++++++------ src/util/WidgetHelpers.hpp | 9 +++++ src/widgets/BaseWindow.cpp | 9 +++-- src/widgets/BaseWindow.hpp | 6 +-- src/widgets/dialogs/EmotePopup.cpp | 32 +++++++++++++--- src/widgets/dialogs/EmotePopup.hpp | 6 +++ src/widgets/splits/SplitInput.cpp | 2 - 12 files changed, 138 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e552c4e58..1d9c45114fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) +- Minor: The size of the emote popup is now saved. (#5415) - Minor: Added the ability to duplicate tabs. (#5277) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp index e08069b2cc4..6c02d5c9522 100644 --- a/src/common/WindowDescriptors.cpp +++ b/src/common/WindowDescriptors.cpp @@ -219,9 +219,15 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) } // Load emote popup position - QJsonObject emote_popup_obj = windowObj.value("emotePopup").toObject(); - layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), - emote_popup_obj.value("y").toInt()); + { + auto emotePopup = windowObj["emotePopup"].toObject(); + layout.emotePopupBounds_ = QRect{ + emotePopup["x"].toInt(), + emotePopup["y"].toInt(), + emotePopup["width"].toInt(), + emotePopup["height"].toInt(), + }; + } layout.windows_.emplace_back(std::move(window)); } diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index b35edf1554e..9964940ca39 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -98,7 +98,7 @@ class WindowLayout { public: // A complete window layout has a single emote popup position that is shared among all windows - QPoint emotePopupPos_; + QRect emotePopupBounds_; std::vector windows_; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 63bc63b366b..69586928883 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -328,14 +328,18 @@ void WindowManager::scrollToMessage(const MessagePtr &message) this->scrollToMessageSignal.invoke(message); } -QPoint WindowManager::emotePopupPos() +QRect WindowManager::emotePopupBounds() const { - return this->emotePopupPos_; + return this->emotePopupBounds_; } -void WindowManager::setEmotePopupPos(QPoint pos) +void WindowManager::setEmotePopupBounds(QRect bounds) { - this->emotePopupPos_ = pos; + if (this->emotePopupBounds_ != bounds) + { + this->emotePopupBounds_ = bounds; + this->queueSave(); + } } void WindowManager::initialize(Settings &settings, const Paths &paths) @@ -371,7 +375,7 @@ void WindowManager::initialize(Settings &settings, const Paths &paths) windowLayout.activateOrAddChannel(desired->provider, desired->name); } - this->emotePopupPos_ = windowLayout.emotePopupPos_; + this->emotePopupBounds_ = windowLayout.emotePopupBounds_; this->applyWindowLayout(windowLayout); } @@ -483,10 +487,12 @@ void WindowManager::save() windowObj.insert("width", rect.width()); windowObj.insert("height", rect.height()); - QJsonObject emotePopupObj; - emotePopupObj.insert("x", this->emotePopupPos_.x()); - emotePopupObj.insert("y", this->emotePopupPos_.y()); - windowObj.insert("emotePopup", emotePopupObj); + windowObj["emotePopup"] = QJsonObject{ + {"x", this->emotePopupBounds_.x()}, + {"y", this->emotePopupBounds_.y()}, + {"width", this->emotePopupBounds_.width()}, + {"height", this->emotePopupBounds_.height()}, + }; // window tabs QJsonArray tabsArr; @@ -753,7 +759,7 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) } // Set emote popup position - this->emotePopupPos_ = layout.emotePopupPos_; + this->emotePopupBounds_ = layout.emotePopupBounds_; for (const auto &windowData : layout.windows_) { @@ -802,10 +808,14 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) // Have to offset x by one because qt moves the window 1px too // far to the left:w - window.setInitialBounds({windowData.geometry_.x(), - windowData.geometry_.y(), - windowData.geometry_.width(), - windowData.geometry_.height()}); + window.setInitialBounds( + { + windowData.geometry_.x(), + windowData.geometry_.y(), + windowData.geometry_.width(), + windowData.geometry_.height(), + }, + widgets::BoundsChecking::Off); } } diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 927a5712977..21040fab094 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -99,8 +99,8 @@ class WindowManager final : public Singleton */ void scrollToMessage(const MessagePtr &message); - QPoint emotePopupPos(); - void setEmotePopupPos(QPoint pos); + QRect emotePopupBounds() const; + void setEmotePopupBounds(QRect bounds); void initialize(Settings &settings, const Paths &paths) override; void save() override; @@ -154,7 +154,7 @@ class WindowManager final : public Singleton bool initialized_ = false; bool shuttingDown_ = false; - QPoint emotePopupPos_; + QRect emotePopupBounds_; std::atomic generation_{0}; diff --git a/src/util/WidgetHelpers.cpp b/src/util/WidgetHelpers.cpp index b5e6fa9a303..cd76dea8091 100644 --- a/src/util/WidgetHelpers.cpp +++ b/src/util/WidgetHelpers.cpp @@ -8,8 +8,7 @@ namespace { -/// Move the `window` into the `screen` geometry if it's not already in there. -void moveWithinScreen(QWidget *window, QScreen *screen, QPoint point) +QPoint applyBounds(QScreen *screen, QPoint point, QSize frameSize, int height) { if (screen == nullptr) { @@ -21,9 +20,6 @@ void moveWithinScreen(QWidget *window, QScreen *screen, QPoint point) bool stickRight = false; bool stickBottom = false; - const auto w = window->frameGeometry().width(); - const auto h = window->frameGeometry().height(); - if (point.x() < bounds.left()) { point.setX(bounds.left()); @@ -32,30 +28,72 @@ void moveWithinScreen(QWidget *window, QScreen *screen, QPoint point) { point.setY(bounds.top()); } - if (point.x() + w > bounds.right()) + if (point.x() + frameSize.width() > bounds.right()) { stickRight = true; - point.setX(bounds.right() - w); + point.setX(bounds.right() - frameSize.width()); } - if (point.y() + h > bounds.bottom()) + if (point.y() + frameSize.height() > bounds.bottom()) { stickBottom = true; - point.setY(bounds.bottom() - h); + point.setY(bounds.bottom() - frameSize.height()); } if (stickRight && stickBottom) { const QPoint globalCursorPos = QCursor::pos(); - point.setY(globalCursorPos.y() - window->height() - 16); + point.setY(globalCursorPos.y() - height - 16); } - window->move(point); + return point; +} + +/// Move the `window` into the `screen` geometry if it's not already in there. +void moveWithinScreen(QWidget *window, QScreen *screen, QPoint point) +{ + auto checked = + applyBounds(screen, point, window->frameSize(), window->height()); + window->move(checked); } } // namespace namespace chatterino::widgets { +QRect checkInitialBounds(QRect initialBounds, BoundsChecking mode) +{ + switch (mode) + { + case BoundsChecking::Off: { + return initialBounds; + } + break; + + case BoundsChecking::CursorPosition: { + return QRect{ + applyBounds(QGuiApplication::screenAt(QCursor::pos()), + initialBounds.topLeft(), initialBounds.size(), + initialBounds.height()), + initialBounds.size(), + }; + } + break; + + case BoundsChecking::DesiredPosition: { + return QRect{ + applyBounds(QGuiApplication::screenAt(initialBounds.topLeft()), + initialBounds.topLeft(), initialBounds.size(), + initialBounds.height()), + initialBounds.size(), + }; + } + break; + default: + assert(false && "Invalid bounds checking mode"); + return initialBounds; + } +} + void moveWindowTo(QWidget *window, QPoint position, BoundsChecking mode) { switch (mode) diff --git a/src/util/WidgetHelpers.hpp b/src/util/WidgetHelpers.hpp index b09e93d0b95..5de90340ffc 100644 --- a/src/util/WidgetHelpers.hpp +++ b/src/util/WidgetHelpers.hpp @@ -3,6 +3,7 @@ class QWidget; class QPoint; class QScreen; +class QRect; namespace chatterino::widgets { @@ -17,6 +18,14 @@ enum class BoundsChecking { DesiredPosition, }; +/// Applies bounds checking to @a initialBounds. +/// +/// @param initialBounds The bounds to check. +/// @param mode The desired bounds checking. +/// @returns The potentially modified bounds. +QRect checkInitialBounds(QRect initialBounds, + 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. /// diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index f3a2afa151a..6d880067ec9 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -29,7 +29,9 @@ # pragma comment(lib, "Dwmapi.lib") # include +# include # include +# include #endif #include "widgets/helper/TitlebarButton.hpp" @@ -251,8 +253,9 @@ BaseWindow::~BaseWindow() DebugCount::decrease("BaseWindow"); } -void BaseWindow::setInitialBounds(const QRect &bounds) +void BaseWindow::setInitialBounds(QRect bounds, widgets::BoundsChecking mode) { + bounds = widgets::checkInitialBounds(bounds, mode); #ifdef USEWINSDK this->initalBounds_ = bounds; #else @@ -260,7 +263,7 @@ void BaseWindow::setInitialBounds(const QRect &bounds) #endif } -QRect BaseWindow::getBounds() +QRect BaseWindow::getBounds() const { #ifdef USEWINSDK return this->currentBounds_; @@ -444,7 +447,7 @@ QWidget *BaseWindow::getLayoutContainer() } } -bool BaseWindow::hasCustomWindowFrame() +bool BaseWindow::hasCustomWindowFrame() const { return BaseWindow::supportsCustomWindowFrame() && this->enableCustomFrame_; } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 46ff56e7e38..946c9e24543 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -45,11 +45,11 @@ class BaseWindow : public BaseWidget QWidget *parent = nullptr); ~BaseWindow() override; - void setInitialBounds(const QRect &bounds); - QRect getBounds(); + void setInitialBounds(QRect bounds, widgets::BoundsChecking mode); + QRect getBounds() const; QWidget *getLayoutContainer(); - bool hasCustomWindowFrame(); + bool hasCustomWindowFrame() const; TitleBarButton *addTitleBarButton(const TitleBarButtonStyle &style, std::function onClicked); EffectLabel *addTitleBarLabel(std::function onClicked); diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index fd80d7e956b..989233ff7e2 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -203,13 +203,18 @@ EmoteMap filterEmoteMap(const QString &text, namespace chatterino { EmotePopup::EmotePopup(QWidget *parent) - : BasePopup(BaseWindow::EnableCustomFrame, parent) + : BasePopup({BaseWindow::EnableCustomFrame, BaseWindow::DisableLayoutSave}, + parent) , search_(new QLineEdit()) , notebook_(new Notebook(this)) { // this->setStayInScreenRect(true); - this->moveTo(getIApp()->getWindows()->emotePopupPos(), - widgets::BoundsChecking::DesiredPosition); + auto bounds = getIApp()->getWindows()->emotePopupBounds(); + if (bounds.size().isEmpty()) + { + bounds.setSize(QSize{300, 500} * this->scale()); + } + this->setInitialBounds(bounds, widgets::BoundsChecking::DesiredPosition); auto *layout = new QVBoxLayout(); this->getLayoutContainer()->setLayout(layout); @@ -594,10 +599,27 @@ void EmotePopup::filterEmotes(const QString &searchText) this->searchView_->show(); } +void EmotePopup::saveBounds() const +{ + getIApp()->getWindows()->setEmotePopupBounds(this->getBounds()); +} + +void EmotePopup::resizeEvent(QResizeEvent *event) +{ + this->saveBounds(); + BasePopup::resizeEvent(event); +} + +void EmotePopup::moveEvent(QMoveEvent *event) +{ + this->saveBounds(); + BasePopup::moveEvent(event); +} + void EmotePopup::closeEvent(QCloseEvent *event) { - getIApp()->getWindows()->setEmotePopupPos(this->pos()); - BaseWindow::closeEvent(event); + this->saveBounds(); + BasePopup::closeEvent(event); } } // namespace chatterino diff --git a/src/widgets/dialogs/EmotePopup.hpp b/src/widgets/dialogs/EmotePopup.hpp index 43dbb45c626..85b5aa5c4f2 100644 --- a/src/widgets/dialogs/EmotePopup.hpp +++ b/src/widgets/dialogs/EmotePopup.hpp @@ -25,6 +25,10 @@ class EmotePopup : public BasePopup pajlada::Signals::Signal linkClicked; +protected: + void resizeEvent(QResizeEvent *event) override; + void moveEvent(QMoveEvent *event) override; + private: ChannelView *globalEmotesView_{}; ChannelView *channelEmotesView_{}; @@ -47,6 +51,8 @@ class EmotePopup : public BasePopup void filterEmotes(const QString &text); void addShortcuts() override; bool eventFilter(QObject *object, QEvent *event) override; + + void saveBounds() const; }; } // namespace chatterino diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index d53b4bad991..141eeed5e6b 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -309,8 +309,6 @@ void SplitInput::openEmotePopup() }); } - this->emotePopup_->resize(int(300 * this->emotePopup_->scale()), - int(500 * this->emotePopup_->scale())); this->emotePopup_->loadChannel(this->split_->getChannel()); this->emotePopup_->show(); this->emotePopup_->raise(); From 2a46ee708e275c70fcee6af3dcd851898d4da1ac Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 1 Jun 2024 13:07:01 +0200 Subject: [PATCH 073/105] Fixed restricted users' usernames not being clickable (#5405) --- CHANGELOG.md | 1 + src/messages/MessageBuilder.cpp | 2 +- src/messages/MessageElement.cpp | 27 +++++++++++- src/messages/MessageElement.hpp | 11 +++-- src/providers/twitch/TwitchMessageBuilder.cpp | 41 +++++++++---------- 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9c45114fc..430291ed032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Minor: Added the ability to duplicate tabs. (#5277) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Bugfix: Fixed restricted users usernames not being clickable. (#5405) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 1ffd5ba28db..cccd540d023 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -763,7 +763,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) auto &&textColor = this->textColor_; if (string.startsWith('@')) { - this->emplace(string, textColor, textColor); + this->emplace(string, "", textColor, textColor); } else { diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 17de1270608..0fb47cd0e33 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -703,11 +703,14 @@ Link LinkElement::getLink() const return {Link::Url, this->linkInfo_.url()}; } -MentionElement::MentionElement(const QString &name, MessageColor fallbackColor_, +MentionElement::MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, MessageColor userColor_) - : TextElement(name, {MessageElementFlag::Text, MessageElementFlag::Mention}) + : TextElement(displayName, + {MessageElementFlag::Text, MessageElementFlag::Mention}) , fallbackColor(fallbackColor_) , userColor(userColor_) + , userLoginName(std::move(loginName_)) { } @@ -735,6 +738,26 @@ void MentionElement::addToContainer(MessageLayoutContainer &container, TextElement::addToContainer(container, flags); } +MessageElement *MentionElement::setLink(const Link &link) +{ + assert(false && "MentionElement::setLink should not be called. Pass " + "through a valid login name in the constructor and it will " + "automatically be a UserInfo link"); + + return TextElement::setLink(link); +} + +Link MentionElement::getLink() const +{ + if (this->userLoginName.isEmpty()) + { + // Some rare mention elements don't have the knowledge of the login name + return {}; + } + + return {Link::UserInfo, this->userLoginName}; +} + // TIMESTAMP TimestampElement::TimestampElement(QTime time) : MessageElement(MessageElementFlag::Timestamp) diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 2c1e98f4e89..49ce762cb91 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -169,7 +169,7 @@ class MessageElement MessageElement(MessageElement &&) = delete; MessageElement &operator=(MessageElement &&) = delete; - MessageElement *setLink(const Link &link); + virtual MessageElement *setLink(const Link &link); MessageElement *setTooltip(const QString &tooltip); MessageElement *setTrailingSpace(bool value); @@ -314,8 +314,8 @@ class LinkElement : public TextElement class MentionElement : public TextElement { public: - MentionElement(const QString &name, MessageColor fallbackColor_, - MessageColor userColor_); + MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, MessageColor userColor_); ~MentionElement() override = default; MentionElement(const MentionElement &) = delete; MentionElement(MentionElement &&) = delete; @@ -325,6 +325,9 @@ class MentionElement : public TextElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + MessageElement *setLink(const Link &link) override; + Link getLink() const override; + private: /** * The color of the element in case the "Colorize @usernames" is disabled @@ -335,6 +338,8 @@ class MentionElement : public TextElement * The color of the element in case the "Colorize @usernames" is enabled **/ MessageColor userColor; + + QString userLoginName; }; // contains emote data and will pick the emote based on : diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 3dfd39720ec..f69264cb3b1 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -770,9 +770,8 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) auto prefixedUsername = '@' + username; auto remainder = string.remove(prefixedUsername); - this->emplace(prefixedUsername, originalTextColor, - textColor) - ->setLink({Link::UserInfo, username}) + this->emplace(prefixedUsername, username, + originalTextColor, textColor) ->setTrailingSpace(remainder.isEmpty()); if (!remainder.isEmpty()) @@ -802,9 +801,8 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } auto remainder = string.remove(username); - this->emplace(username, originalTextColor, + this->emplace(username, username, originalTextColor, textColor) - ->setLink({Link::UserInfo, username}) ->setTrailingSpace(remainder.isEmpty()); if (!remainder.isEmpty()) @@ -1821,8 +1819,10 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, } } - builder->emplace(username, MessageColor::System, color) - ->setLink({Link::UserInfo, username}) + // TODO: Ensure we make use of display name / username(login name) correctly here + builder + ->emplace(username, username, MessageColor::System, + color) ->setTrailingSpace(false); } } @@ -1867,9 +1867,8 @@ void TwitchMessageBuilder::listOfUsersSystemMessage( } builder - ->emplace(user.userName, MessageColor::System, - color) - ->setLink({Link::UserInfo, user.userLogin}) + ->emplace(user.userName, user.userLogin, + MessageColor::System, color) ->setTrailingSpace(false); } @@ -1938,8 +1937,8 @@ MessagePtr TwitchMessageBuilder::makeAutomodInfoMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder.emplace("AutoMod:", AUTOMOD_USER_COLOR, - AUTOMOD_USER_COLOR); + builder.emplace("AutoMod:", MessageElementFlag::Text, + AUTOMOD_USER_COLOR, FontStyle::ChatMediumBold); switch (action.type) { case AutomodInfoAction::OnHold: { @@ -1993,8 +1992,9 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( builder.emplace(makeAutoModBadge(), MessageElementFlag::BadgeChannelAuthority); // AutoMod "username" - builder2.emplace("AutoMod:", AUTOMOD_USER_COLOR, - AUTOMOD_USER_COLOR); + builder2.emplace("AutoMod:", MessageElementFlag::Text, + AUTOMOD_USER_COLOR, + FontStyle::ChatMediumBold); // AutoMod header message builder.emplace( ("Held a message for reason: " + action.reason + @@ -2041,10 +2041,9 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( builder2.message().flags.set(MessageFlag::AutoModOffendingMessage); // sender username - builder2 - .emplace(action.target.displayName + ":", - MessageColor::Text, action.target.color) - ->setLink({Link::UserInfo, action.target.login}); + builder2.emplace(action.target.displayName + ":", + action.target.login, MessageColor::Text, + action.target.color); // sender's message caught by AutoMod builder2.emplace(action.message, MessageElementFlag::Text, MessageColor::Text); @@ -2239,9 +2238,9 @@ std::pair TwitchMessageBuilder::makeLowTrustUserMessage( appendBadges(&builder2, action.senderBadges, {}, twitchChannel); // sender username - builder2.emplace(action.suspiciousUserDisplayName + ":", - MessageColor::Text, - action.suspiciousUserColor); + builder2.emplace( + action.suspiciousUserDisplayName + ":", action.suspiciousUserLogin, + MessageColor::Text, action.suspiciousUserColor); // sender's message caught by AutoMod for (const auto &fragment : action.fragments) From b6dc5d9e03aad21f1b8c76c588a1a627fbe41c9e Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 1 Jun 2024 14:56:40 +0200 Subject: [PATCH 074/105] chore: refactor TwitchIrcServer (#5421) --- CHANGELOG.md | 1 + mocks/include/mocks/EmptyApplication.hpp | 7 ++ mocks/include/mocks/TwitchIrcServer.hpp | 74 ++++++++++++++++++ src/Application.cpp | 77 ++++++++++++------- src/Application.hpp | 9 +-- src/controllers/commands/builtin/Misc.cpp | 13 ++-- .../commands/builtin/twitch/SendWhisper.cpp | 6 +- .../notifications/NotificationController.cpp | 6 +- src/controllers/plugins/api/ChannelRef.cpp | 4 +- src/providers/irc/AbstractIrcServer.hpp | 33 ++++++-- src/providers/irc/IrcServer.cpp | 2 +- src/providers/twitch/IrcMessageHandler.cpp | 56 +++++++------- src/providers/twitch/IrcMessageHandler.hpp | 11 ++- src/providers/twitch/TwitchChannel.cpp | 41 +++++----- src/providers/twitch/TwitchIrcServer.cpp | 44 ++++++++++- src/providers/twitch/TwitchIrcServer.hpp | 38 ++++++++- src/singletons/NativeMessaging.cpp | 17 ++-- src/singletons/StreamerMode.cpp | 4 +- src/singletons/WindowManager.cpp | 16 ++-- src/widgets/FramelessEmbedWindow.cpp | 3 +- src/widgets/Window.cpp | 17 ++-- src/widgets/dialogs/SelectChannelDialog.cpp | 14 ++-- src/widgets/dialogs/UserInfoPopup.cpp | 12 +-- src/widgets/dialogs/switcher/NewPopupItem.cpp | 3 +- src/widgets/dialogs/switcher/NewTabItem.cpp | 3 +- src/widgets/helper/ChannelView.cpp | 21 ++--- src/widgets/settingspages/GeneralPage.cpp | 2 +- src/widgets/splits/Split.cpp | 2 +- 28 files changed, 373 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430291ed032..1ac43b47a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) +- Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421) - Dev: Reduced the amount of scale events. (#5404, #5406) - Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 5d5ea1064ea..54906f56c75 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -118,6 +118,13 @@ class EmptyApplication : public IApplication return nullptr; } + IAbstractIrcServer *getTwitchAbstract() override + { + assert(false && "EmptyApplication::getTwitchAbstract was called " + "without being initialized"); + return nullptr; + } + PubSub *getTwitchPubSub() override { assert(false && "getTwitchPubSub was called without being initialized"); diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp index 976d6f2addd..42abc30ec5b 100644 --- a/mocks/include/mocks/TwitchIrcServer.hpp +++ b/mocks/include/mocks/TwitchIrcServer.hpp @@ -2,8 +2,13 @@ #include "mocks/Channel.hpp" #include "providers/bttv/BttvEmotes.hpp" +#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/ffz/FfzEmotes.hpp" +#include "providers/seventv/eventapi/Client.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Message.hpp" #include "providers/seventv/SeventvEmotes.hpp" +#include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/TwitchIrcServer.hpp" namespace chatterino::mock { @@ -16,22 +21,91 @@ class MockTwitchIrcServer : public ITwitchIrcServer std::shared_ptr(new MockChannel("testaccount_420"))) , watchingChannel(this->watchingChannelInner, Channel::Type::TwitchWatching) + , whispersChannel(std::shared_ptr(new MockChannel("whispers"))) + , mentionsChannel(std::shared_ptr(new MockChannel("forsen3"))) + , liveChannel(std::shared_ptr(new MockChannel("forsen"))) + , automodChannel(std::shared_ptr(new MockChannel("forsen2"))) { } + void forEachChannelAndSpecialChannels( + std::function func) override + { + // + } + + std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) override + { + return {}; + } + + void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) override + { + // + } + + std::unique_ptr &getBTTVLiveUpdates() override + { + return this->bttvLiveUpdates; + } + + std::unique_ptr &getSeventvEventAPI() override + { + return this->seventvEventAPI; + } + const IndirectChannel &getWatchingChannel() const override { return this->watchingChannel; } + void setWatchingChannel(ChannelPtr newWatchingChannel) override + { + this->watchingChannel.reset(newWatchingChannel); + } + QString getLastUserThatWhisperedMe() const override { return this->lastUserThatWhisperedMe; } + void setLastUserThatWhisperedMe(const QString &user) override + { + this->lastUserThatWhisperedMe = user; + } + + ChannelPtr getWhispersChannel() const override + { + return this->whispersChannel; + } + + ChannelPtr getMentionsChannel() const override + { + return this->mentionsChannel; + } + + ChannelPtr getLiveChannel() const override + { + return this->liveChannel; + } + + ChannelPtr getAutomodChannel() const override + { + return this->automodChannel; + } + ChannelPtr watchingChannelInner; IndirectChannel watchingChannel; + ChannelPtr whispersChannel; + ChannelPtr mentionsChannel; + ChannelPtr liveChannel; + ChannelPtr automodChannel; QString lastUserThatWhisperedMe{"forsen"}; + + std::unique_ptr bttvLiveUpdates; + std::unique_ptr seventvEventAPI; }; } // namespace chatterino::mock diff --git a/src/Application.cpp b/src/Application.cpp index e412fe8f4cf..5d70316bf1d 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -13,6 +13,7 @@ #include "controllers/sound/ISoundController.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" +#include "providers/irc/AbstractIrcServer.hpp" #include "providers/links/LinkResolver.hpp" #include "providers/seventv/SeventvAPI.hpp" #include "providers/seventv/SeventvEmotes.hpp" @@ -131,7 +132,7 @@ Application::Application(Settings &_settings, const Paths &paths, , commands(&this->emplace()) , notifications(&this->emplace()) , highlights(&this->emplace()) - , twitch(&this->emplace()) + , twitch(new TwitchIrcServer) , ffzBadges(&this->emplace()) , seventvBadges(&this->emplace()) , userData(&this->emplace(new UserDataController(paths))) @@ -170,6 +171,7 @@ void Application::fakeDtor() this->bttvEmotes.reset(); this->ffzEmotes.reset(); this->seventvEmotes.reset(); + // this->twitch.reset(); this->fonts.reset(); } @@ -483,7 +485,14 @@ ITwitchIrcServer *Application::getTwitch() { assertInGuiThread(); - return this->twitch; + return this->twitch.get(); +} + +IAbstractIrcServer *Application::getTwitchAbstract() +{ + assertInGuiThread(); + + return this->twitch.get(); } PubSub *Application::getTwitchPubSub() @@ -865,17 +874,25 @@ void Application::initPubSub() chan->addMessage(p.first); chan->addMessage(p.second); - getApp()->twitch->automodChannel->addMessage( - p.first); - getApp()->twitch->automodChannel->addMessage( - p.second); + getIApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.first); + getIApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.second); if (getSettings()->showAutomodInMentions) { - getApp()->twitch->mentionsChannel->addMessage( - p.first); - getApp()->twitch->mentionsChannel->addMessage( - p.second); + getIApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.first); + getIApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.second); } }); } @@ -984,7 +1001,9 @@ void Application::initPubSub() void Application::initBttvLiveUpdates() { - if (!this->twitch->bttvLiveUpdates) + auto &bttvLiveUpdates = this->twitch->getBTTVLiveUpdates(); + + if (!bttvLiveUpdates) { qCDebug(chatterinoBttv) << "Skipping initialization of Live Updates as it's disabled"; @@ -993,8 +1012,8 @@ void Application::initBttvLiveUpdates() // We can safely ignore these signal connections since the twitch object will always // be destroyed before the Application - std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteAdded.connect( - [&](const auto &data) { + std::ignore = + bttvLiveUpdates->signals_.emoteAdded.connect([&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1004,8 +1023,8 @@ void Application::initBttvLiveUpdates() } }); }); - std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteUpdated.connect( - [&](const auto &data) { + std::ignore = + bttvLiveUpdates->signals_.emoteUpdated.connect([&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1015,8 +1034,8 @@ void Application::initBttvLiveUpdates() } }); }); - std::ignore = this->twitch->bttvLiveUpdates->signals_.emoteRemoved.connect( - [&](const auto &data) { + std::ignore = + bttvLiveUpdates->signals_.emoteRemoved.connect([&](const auto &data) { auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); postToThread([chan, data] { @@ -1026,12 +1045,14 @@ void Application::initBttvLiveUpdates() } }); }); - this->twitch->bttvLiveUpdates->start(); + bttvLiveUpdates->start(); } void Application::initSeventvEventAPI() { - if (!this->twitch->seventvEventAPI) + auto &seventvEventAPI = this->twitch->getSeventvEventAPI(); + + if (!seventvEventAPI) { qCDebug(chatterinoSeventvEventAPI) << "Skipping initialization as the EventAPI is disabled"; @@ -1040,8 +1061,8 @@ void Application::initSeventvEventAPI() // We can safely ignore these signal connections since the twitch object will always // be destroyed before the Application - std::ignore = this->twitch->seventvEventAPI->signals_.emoteAdded.connect( - [&](const auto &data) { + std::ignore = + seventvEventAPI->signals_.emoteAdded.connect([&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( data.emoteSetID, [data](TwitchChannel &chan) { @@ -1049,8 +1070,8 @@ void Application::initSeventvEventAPI() }); }); }); - std::ignore = this->twitch->seventvEventAPI->signals_.emoteUpdated.connect( - [&](const auto &data) { + std::ignore = + seventvEventAPI->signals_.emoteUpdated.connect([&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( data.emoteSetID, [data](TwitchChannel &chan) { @@ -1058,8 +1079,8 @@ void Application::initSeventvEventAPI() }); }); }); - std::ignore = this->twitch->seventvEventAPI->signals_.emoteRemoved.connect( - [&](const auto &data) { + std::ignore = + seventvEventAPI->signals_.emoteRemoved.connect([&](const auto &data) { postToThread([this, data] { this->twitch->forEachSeventvEmoteSet( data.emoteSetID, [data](TwitchChannel &chan) { @@ -1067,15 +1088,15 @@ void Application::initSeventvEventAPI() }); }); }); - std::ignore = this->twitch->seventvEventAPI->signals_.userUpdated.connect( - [&](const auto &data) { + std::ignore = + seventvEventAPI->signals_.userUpdated.connect([&](const auto &data) { this->twitch->forEachSeventvUser(data.userID, [data](TwitchChannel &chan) { chan.updateSeventvUser(data); }); }); - this->twitch->seventvEventAPI->start(); + seventvEventAPI->start(); } Application *getApp() diff --git a/src/Application.hpp b/src/Application.hpp index f846ff00b07..795a9f6d976 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -54,6 +54,7 @@ class FfzEmotes; class SeventvEmotes; class ILinkResolver; class IStreamerMode; +class IAbstractIrcServer; class IApplication { @@ -77,6 +78,7 @@ class IApplication virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; virtual ITwitchIrcServer *getTwitch() = 0; + virtual IAbstractIrcServer *getTwitchAbstract() = 0; virtual PubSub *getTwitchPubSub() = 0; virtual Logging *getChatLogger() = 0; virtual IChatterinoBadges *getChatterinoBadges() = 0; @@ -147,11 +149,7 @@ class Application : public IApplication CommandController *const commands{}; NotificationController *const notifications{}; HighlightController *const highlights{}; - -public: - TwitchIrcServer *const twitch{}; - -private: + std::unique_ptr twitch; FfzBadges *const ffzBadges{}; SeventvBadges *const seventvBadges{}; UserDataController *const userData{}; @@ -191,6 +189,7 @@ class Application : public IApplication NotificationController *getNotifications() override; HighlightController *getHighlights() override; ITwitchIrcServer *getTwitch() override; + IAbstractIrcServer *getTwitchAbstract() override; PubSub *getTwitchPubSub() override; Logging *getChatLogger() override; FfzBadges *getFfzBadges() override; diff --git a/src/controllers/commands/builtin/Misc.cpp b/src/controllers/commands/builtin/Misc.cpp index c0b62c2d9f9..e1940271a36 100644 --- a/src/controllers/commands/builtin/Misc.cpp +++ b/src/controllers/commands/builtin/Misc.cpp @@ -390,9 +390,9 @@ QString popup(const CommandContext &ctx) } // Open channel passed as argument in a popup - auto *app = getApp(); - auto targetChannel = app->twitch->getOrAddChannel(target); - app->getWindows()->openInPopup(targetChannel); + auto targetChannel = + getIApp()->getTwitchAbstract()->getOrAddChannel(target); + getIApp()->getWindows()->openInPopup(targetChannel); return ""; } @@ -533,7 +533,8 @@ QString sendRawMessage(const CommandContext &ctx) if (ctx.channel->isTwitchChannel()) { - getApp()->twitch->sendRawMessage(ctx.words.mid(1).join(" ")); + getIApp()->getTwitchAbstract()->sendRawMessage( + ctx.words.mid(1).join(" ")); } else { @@ -566,7 +567,7 @@ QString injectFakeMessage(const CommandContext &ctx) } auto ircText = ctx.words.mid(1).join(" "); - getApp()->twitch->addFakeMessage(ircText); + getIApp()->getTwitchAbstract()->addFakeMessage(ircText); return ""; } @@ -667,7 +668,7 @@ QString openUsercard(const CommandContext &ctx) stripChannelName(channelName); ChannelPtr channelTemp = - getApp()->twitch->getChannelOrEmpty(channelName); + getIApp()->getTwitchAbstract()->getChannelOrEmpty(channelName); if (channelTemp->isEmpty()) { diff --git a/src/controllers/commands/builtin/twitch/SendWhisper.cpp b/src/controllers/commands/builtin/twitch/SendWhisper.cpp index 6b4cc25bfbc..1d804604624 100644 --- a/src/controllers/commands/builtin/twitch/SendWhisper.cpp +++ b/src/controllers/commands/builtin/twitch/SendWhisper.cpp @@ -92,7 +92,7 @@ QString formatWhisperError(HelixWhisperError error, const QString &message) bool appendWhisperMessageWordsLocally(const QStringList &words) { - auto *app = getApp(); + auto *app = getIApp(); MessageBuilder b; @@ -177,7 +177,7 @@ bool appendWhisperMessageWordsLocally(const QStringList &words) b->flags.set(MessageFlag::Whisper); auto messagexD = b.release(); - app->twitch->whispersChannel->addMessage(messagexD); + getIApp()->getTwitch()->getWhispersChannel()->addMessage(messagexD); auto overrideFlags = std::optional(messagexD->flags); overrideFlags->set(MessageFlag::DoNotLog); @@ -186,7 +186,7 @@ bool appendWhisperMessageWordsLocally(const QStringList &words) !(getSettings()->streamerModeSuppressInlineWhispers && getIApp()->getStreamerMode()->isEnabled())) { - app->twitch->forEachChannel( + app->getTwitchAbstract()->forEachChannel( [&messagexD, overrideFlags](ChannelPtr _channel) { _channel->addMessage(messagexD, overrideFlags); }); diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 745cf383e24..cfab7242fb3 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -124,7 +124,7 @@ void NotificationController::fetchFakeChannels() for (std::vector::size_type i = 0; i < channelMap[Platform::Twitch].raw().size(); i++) { - auto chan = getApp()->twitch->getChannelOrEmpty( + auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty( channelMap[Platform::Twitch].raw()[i]); if (chan->isEmpty()) { @@ -202,7 +202,7 @@ void NotificationController::checkStream(bool live, QString channelName) } MessageBuilder builder; TwitchMessageBuilder::liveMessage(channelName, &builder); - getApp()->twitch->liveChannel->addMessage(builder.release()); + getIApp()->getTwitch()->getLiveChannel()->addMessage(builder.release()); // Indicate that we have pushed notifications for this stream fakeTwitchChannels.push_back(channelName); @@ -217,7 +217,7 @@ void NotificationController::removeFakeChannel(const QString channelName) fakeTwitchChannels.erase(it); // "delete" old 'CHANNEL is live' message LimitedQueueSnapshot snapshot = - getApp()->twitch->liveChannel->getMessageSnapshot(); + getIApp()->getTwitch()->getLiveChannel()->getMessageSnapshot(); int snapshotLength = snapshot.size(); // MSVC hates this code if the parens are not there diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index 986fbbac359..1e592d0db1c 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -300,7 +300,7 @@ int ChannelRef::get_by_name(lua_State *L) lua_pushnil(L); return 1; } - auto chn = getApp()->twitch->getChannelOrEmpty(name); + auto chn = getIApp()->getTwitchAbstract()->getChannelOrEmpty(name); lua::push(L, chn); return 1; } @@ -324,7 +324,7 @@ int ChannelRef::get_by_twitch_id(lua_State *L) lua_pushnil(L); return 1; } - auto chn = getApp()->twitch->getChannelOrEmptyByID(id); + auto chn = getIApp()->getTwitch()->getChannelOrEmptyByID(id); lua::push(L, chn); return 1; diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 7a0e1ee639a..0b626f9e0be 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -17,7 +17,24 @@ class Channel; using ChannelPtr = std::shared_ptr; class RatelimitBucket; -class AbstractIrcServer : public QObject +class IAbstractIrcServer +{ +public: + virtual void connect() = 0; + + virtual void sendRawMessage(const QString &rawMessage) = 0; + + virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0; + virtual ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) = 0; + + virtual void addFakeMessage(const QString &data) = 0; + + virtual void addGlobalSystemMessage(const QString &messageText) = 0; + + virtual void forEachChannel(std::function func) = 0; +}; + +class AbstractIrcServer : public IAbstractIrcServer, public QObject { public: enum ConnectionType { Read = 1, Write = 2, Both = 3 }; @@ -33,27 +50,27 @@ class AbstractIrcServer : public QObject void initializeIrc(); // connection - void connect(); + void connect() final; void disconnect(); void sendMessage(const QString &channelName, const QString &message); - virtual void sendRawMessage(const QString &rawMessage); + void sendRawMessage(const QString &rawMessage) override; // channels - ChannelPtr getOrAddChannel(const QString &dirtyChannelName); - ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName); + ChannelPtr getOrAddChannel(const QString &dirtyChannelName) final; + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) final; std::vector> getChannels(); // signals pajlada::Signals::NoArgSignal connected; pajlada::Signals::NoArgSignal disconnected; - void addFakeMessage(const QString &data); + void addFakeMessage(const QString &data) final; - void addGlobalSystemMessage(const QString &messageText); + void addGlobalSystemMessage(const QString &messageText) final; // iteration - void forEachChannel(std::function func); + void forEachChannel(std::function func) final; protected: AbstractIrcServer(); diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 8d54d79f7b0..c90211fcb1a 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -244,7 +244,7 @@ void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) if (highlighted && showInMentions) { - getApp()->twitch->mentionsChannel->addMessage(msg); + getIApp()->getTwitch()->getMentionsChannel()->addMessage(msg); } } else diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 9dd2d7a4b70..d0600b8fa53 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -13,6 +13,7 @@ #include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" #include "messages/MessageThread.hpp" +#include "providers/irc/AbstractIrcServer.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccountManager.hpp" @@ -169,7 +170,7 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, } ChannelPtr channelOrEmptyByTarget(const QString &target, - TwitchIrcServer &server) + IAbstractIrcServer &server) { QString channelName; if (!trimChannelName(target, channelName)) @@ -677,9 +678,10 @@ std::vector IrcMessageHandler::parseMessageWithReply( } void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, - TwitchIrcServer &server) + ITwitchIrcServer &twitchServer, + IAbstractIrcServer &abstractIrcServer) { - auto chan = channelOrEmptyByTarget(message->target(), server); + auto chan = channelOrEmptyByTarget(message->target(), abstractIrcServer); if (chan->isEmpty()) { return; @@ -710,8 +712,8 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, // https://mm2pl.github.io/emoji_rfc.pdf for more details this->addMessage( message, chan, - message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), server, - false, message->isAction()); + message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), + twitchServer, false, message->isAction()); if (message->tags().contains(u"pinned-chat-paid-amount"_s)) { @@ -733,7 +735,7 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) { return; } - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(chanName); auto *twitchChannel = dynamic_cast(chan.get()); if (!twitchChannel) @@ -795,7 +797,7 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) } // get channel - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(chanName); if (chan->isEmpty()) { @@ -839,7 +841,7 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) } // get channel - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(chanName); if (chan->isEmpty()) { @@ -888,7 +890,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) return; } - auto c = getApp()->twitch->getChannelOrEmpty(channelName); + auto c = getIApp()->getTwitchAbstract()->getChannelOrEmpty(channelName); if (c->isEmpty()) { return; @@ -943,7 +945,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) args.isReceivedWhisper = true; - auto *c = getApp()->twitch->whispersChannel.get(); + auto *c = getIApp()->getTwitch()->getWhispersChannel().get(); TwitchMessageBuilder builder( c, ircMessage, args, @@ -959,11 +961,11 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) MessagePtr message = builder.build(); builder.triggerHighlights(); - getApp()->twitch->lastUserThatWhisperedMe.set(builder.userName); + getIApp()->getTwitch()->setLastUserThatWhisperedMe(builder.userName); if (message->flags.has(MessageFlag::ShowInMentions)) { - getApp()->twitch->mentionsChannel->addMessage(message); + getIApp()->getTwitch()->getMentionsChannel()->addMessage(message); } c->addMessage(message); @@ -976,15 +978,16 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) !(getSettings()->streamerModeSuppressInlineWhispers && getIApp()->getStreamerMode()->isEnabled())) { - getApp()->twitch->forEachChannel( + getIApp()->getTwitchAbstract()->forEachChannel( [&message, overrideFlags](ChannelPtr channel) { channel->addMessage(message, overrideFlags); }); } } -void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, - TwitchIrcServer &server) +void IrcMessageHandler::handleUserNoticeMessage( + Communi::IrcMessage *message, ITwitchIrcServer &twitchServer, + IAbstractIrcServer &abstractIrcServer) { auto tags = message->tags(); auto parameters = message->parameters(); @@ -997,7 +1000,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, content = parameters[1]; } - auto chn = server.getChannelOrEmpty(target); + auto chn = abstractIrcServer.getChannelOrEmpty(target); if (isIgnoredMessage({ .message = content, .twitchUserID = tags.value("user-id").toString(), @@ -1013,7 +1016,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, // Messages are not required, so they might be empty if (!content.isEmpty()) { - this->addMessage(message, chn, content, server, true, false); + this->addMessage(message, chn, content, twitchServer, true, false); } } @@ -1090,7 +1093,7 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, return; } - auto chan = server.getChannelOrEmpty(channelName); + auto chan = abstractIrcServer.getChannelOrEmpty(channelName); if (!chan->isEmpty()) { @@ -1111,7 +1114,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { // Notice wasn't targeted at a single channel, send to all twitch // channels - getApp()->twitch->forEachChannelAndSpecialChannels( + getIApp()->getTwitch()->forEachChannelAndSpecialChannels( [msg](const auto &c) { c->addMessage(msg); }); @@ -1119,7 +1122,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) return; } - auto channel = getApp()->twitch->getChannelOrEmpty(channelName); + auto channel = + getIApp()->getTwitchAbstract()->getChannelOrEmpty(channelName); if (channel->isEmpty()) { @@ -1202,8 +1206,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) { - auto channel = - getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = getIApp()->getTwitchAbstract()->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); auto *twitchChannel = dynamic_cast(channel.get()); if (!twitchChannel) @@ -1225,8 +1229,8 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) { - auto channel = - getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = getIApp()->getTwitchAbstract()->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); auto *twitchChannel = dynamic_cast(channel.get()); if (!twitchChannel) @@ -1311,7 +1315,7 @@ void IrcMessageHandler::setSimilarityFlags(const MessagePtr &message, void IrcMessageHandler::addMessage(Communi::IrcMessage *message, const ChannelPtr &chan, const QString &originalContent, - TwitchIrcServer &server, bool isSub, + ITwitchIrcServer &server, bool isSub, bool isAction) { if (chan->isEmpty()) @@ -1446,7 +1450,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, if (highlighted && showInMentions) { - server.mentionsChannel->addMessage(msg); + server.getMentionsChannel()->addMessage(msg); } chan->addMessage(msg); diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index 26c21f6da64..60dbacaf356 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -9,7 +9,8 @@ namespace chatterino { -class TwitchIrcServer; +class IAbstractIrcServer; +class ITwitchIrcServer; class Channel; using ChannelPtr = std::shared_ptr; struct Message; @@ -38,7 +39,8 @@ class IrcMessageHandler std::vector &otherLoaded); void handlePrivMessage(Communi::IrcPrivateMessage *message, - TwitchIrcServer &server); + ITwitchIrcServer &twitchServer, + IAbstractIrcServer &abstractIrcServer); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); @@ -48,7 +50,8 @@ class IrcMessageHandler void handleWhisperMessage(Communi::IrcMessage *ircMessage); void handleUserNoticeMessage(Communi::IrcMessage *message, - TwitchIrcServer &server); + ITwitchIrcServer &twitchServer, + IAbstractIrcServer &abstractIrcServer); void handleNoticeMessage(Communi::IrcNoticeMessage *message); @@ -56,7 +59,7 @@ class IrcMessageHandler void handlePartMessage(Communi::IrcMessage *message); void addMessage(Communi::IrcMessage *message, const ChannelPtr &chan, - const QString &originalContent, TwitchIrcServer &server, + const QString &originalContent, ITwitchIrcServer &server, bool isSub, bool isAction); private: diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index c478b919133..4339d6c2ec6 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -176,7 +176,8 @@ TwitchChannel::TwitchChannel(const QString &name) TwitchMessageBuilder::liveMessage(this->getDisplayName(), &builder2); builder2.message().id = this->roomId(); - getApp()->twitch->liveChannel->addMessage(builder2.release()); + getIApp()->getTwitch()->getLiveChannel()->addMessage( + builder2.release()); // Notify on all channels with a ping sound if (getSettings()->notificationOnAnyChannel && @@ -198,7 +199,7 @@ TwitchChannel::TwitchChannel(const QString &name) // "delete" old 'CHANNEL is live' message LimitedQueueSnapshot snapshot = - getApp()->twitch->liveChannel->getMessageSnapshot(); + getIApp()->getTwitch()->getLiveChannel()->getMessageSnapshot(); int snapshotLength = snapshot.size(); // MSVC hates this code if the parens are not there @@ -237,17 +238,18 @@ TwitchChannel::~TwitchChannel() return; } - getApp()->twitch->dropSeventvChannel(this->seventvUserID_, - this->seventvEmoteSetID_); + getIApp()->getTwitch()->dropSeventvChannel(this->seventvUserID_, + this->seventvEmoteSetID_); - if (getApp()->twitch->bttvLiveUpdates) + if (getIApp()->getTwitch()->getBTTVLiveUpdates()) { - getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId()); + getIApp()->getTwitch()->getBTTVLiveUpdates()->partChannel( + this->roomId()); } - if (getApp()->twitch->seventvEventAPI) + if (getIApp()->getTwitch()->getSeventvEventAPI()) { - getApp()->twitch->seventvEventAPI->unsubscribeTwitchChannel( + getIApp()->getTwitch()->getSeventvEventAPI()->unsubscribeTwitchChannel( this->roomId()); } } @@ -425,7 +427,7 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) << "] Channel point reward added:" << reward.id << "," << reward.title << "," << reward.isUserInputRequired; - auto *server = getApp()->twitch; + auto *server = getIApp()->getTwitch(); auto it = std::remove_if( this->waitingRedemptions_.begin(), this->waitingRedemptions_.end(), [&](const QueuedRedemption &msg) { @@ -776,7 +778,7 @@ bool TwitchChannel::canReconnect() const void TwitchChannel::reconnect() { - getApp()->twitch->connect(); + getIApp()->getTwitchAbstract()->connect(); } QString TwitchChannel::roomId() const @@ -891,7 +893,7 @@ const QString &TwitchChannel::seventvEmoteSetID() const void TwitchChannel::joinBttvChannel() const { - if (getApp()->twitch->bttvLiveUpdates) + if (getIApp()->getTwitch()->getBTTVLiveUpdates()) { const auto currentAccount = getIApp()->getAccounts()->twitch.getCurrent(); @@ -900,8 +902,8 @@ void TwitchChannel::joinBttvChannel() const { userName = currentAccount->getUserName(); } - getApp()->twitch->bttvLiveUpdates->joinChannel(this->roomId(), - userName); + getIApp()->getTwitch()->getBTTVLiveUpdates()->joinChannel( + this->roomId(), userName); } } @@ -1048,14 +1050,14 @@ void TwitchChannel::updateSeventvData(const QString &newUserID, this->seventvUserID_ = newUserID; this->seventvEmoteSetID_ = newEmoteSetID; runInGuiThread([this, oldUserID, oldEmoteSetID]() { - if (getApp()->twitch->seventvEventAPI) + if (getIApp()->getTwitch()->getSeventvEventAPI()) { - getApp()->twitch->seventvEventAPI->subscribeUser( + getIApp()->getTwitch()->getSeventvEventAPI()->subscribeUser( this->seventvUserID_, this->seventvEmoteSetID_); if (oldUserID || oldEmoteSetID) { - getApp()->twitch->dropSeventvChannel( + getIApp()->getTwitch()->dropSeventvChannel( oldUserID.value_or(QString()), oldEmoteSetID.value_or(QString())); } @@ -1251,7 +1253,8 @@ void TwitchChannel::loadRecentMessages() tc->addRecentChatter(msg->displayName); } - getApp()->twitch->mentionsChannel->fillInMissingMessages(msgs); + getIApp()->getTwitch()->getMentionsChannel()->fillInMissingMessages( + msgs); }, [weak]() { auto shared = weak.lock(); @@ -1841,9 +1844,9 @@ void TwitchChannel::updateSevenTVActivity() void TwitchChannel::listenSevenTVCosmetics() const { - if (getApp()->twitch->seventvEventAPI) + if (getIApp()->getTwitch()->getSeventvEventAPI()) { - getApp()->twitch->seventvEventAPI->subscribeTwitchChannel( + getIApp()->getTwitch()->getSeventvEventAPI()->subscribeTwitchChannel( this->roomId()); } } diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index f591e8f310e..e318d45af19 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -263,7 +263,7 @@ std::shared_ptr TwitchIrcServer::createChannel( void TwitchIrcServer::privateMessageReceived( Communi::IrcPrivateMessage *message) { - IrcMessageHandler::instance().handlePrivMessage(message, *this); + IrcMessageHandler::instance().handlePrivMessage(message, *this, *this); } void TwitchIrcServer::readConnectionMessageReceived( @@ -310,7 +310,7 @@ void TwitchIrcServer::readConnectionMessageReceived( } else if (command == "USERNOTICE") { - handler.handleUserNoticeMessage(message, *this); + handler.handleUserNoticeMessage(message, *this, *this); } else if (command == "NOTICE") { @@ -645,16 +645,56 @@ void TwitchIrcServer::onReplySendRequested( sent = true; } +std::unique_ptr &TwitchIrcServer::getBTTVLiveUpdates() +{ + return this->bttvLiveUpdates; +} + +std::unique_ptr &TwitchIrcServer::getSeventvEventAPI() +{ + return this->seventvEventAPI; +} + const IndirectChannel &TwitchIrcServer::getWatchingChannel() const { return this->watchingChannel; } +void TwitchIrcServer::setWatchingChannel(ChannelPtr newWatchingChannel) +{ + this->watchingChannel.reset(newWatchingChannel); +} + +ChannelPtr TwitchIrcServer::getWhispersChannel() const +{ + return this->whispersChannel; +} + +ChannelPtr TwitchIrcServer::getMentionsChannel() const +{ + return this->mentionsChannel; +} + +ChannelPtr TwitchIrcServer::getLiveChannel() const +{ + return this->liveChannel; +} + +ChannelPtr TwitchIrcServer::getAutomodChannel() const +{ + return this->automodChannel; +} + QString TwitchIrcServer::getLastUserThatWhisperedMe() const { return this->lastUserThatWhisperedMe.get(); } +void TwitchIrcServer::setLastUserThatWhisperedMe(const QString &user) +{ + this->lastUserThatWhisperedMe.set(user); +} + void TwitchIrcServer::reloadBTTVGlobalEmotes() { getIApp()->getBttvEmotes()->loadEmotes(); diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 5fef4908492..15e3af990bc 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -27,9 +27,27 @@ class ITwitchIrcServer public: virtual ~ITwitchIrcServer() = default; + virtual void forEachChannelAndSpecialChannels( + std::function func) = 0; + + virtual std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) = 0; + + virtual void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) = 0; + + virtual std::unique_ptr &getBTTVLiveUpdates() = 0; + virtual std::unique_ptr &getSeventvEventAPI() = 0; + virtual const IndirectChannel &getWatchingChannel() const = 0; + virtual void setWatchingChannel(ChannelPtr newWatchingChannel) = 0; + virtual ChannelPtr getWhispersChannel() const = 0; + virtual ChannelPtr getMentionsChannel() const = 0; + virtual ChannelPtr getLiveChannel() const = 0; + virtual ChannelPtr getAutomodChannel() const = 0; virtual QString getLastUserThatWhisperedMe() const = 0; + virtual void setLastUserThatWhisperedMe(const QString &user) = 0; // Update this interface with TwitchIrcServer methods as needed }; @@ -44,9 +62,11 @@ class TwitchIrcServer final : public AbstractIrcServer, void initialize(Settings &settings, const Paths &paths) override; - void forEachChannelAndSpecialChannels(std::function func); + void forEachChannelAndSpecialChannels( + std::function func) override; - std::shared_ptr getChannelOrEmptyByID(const QString &channelID); + std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) override; void reloadBTTVGlobalEmotes(); void reloadAllBTTVChannelEmotes(); @@ -68,8 +88,10 @@ class TwitchIrcServer final : public AbstractIrcServer, * It's currently not possible to share emote sets among users, * but it's a commonly requested feature. */ - void dropSeventvChannel(const QString &userID, const QString &emoteSetID); + void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) override; +private: Atomic lastUserThatWhisperedMe; const ChannelPtr whispersChannel; @@ -81,9 +103,19 @@ class TwitchIrcServer final : public AbstractIrcServer, std::unique_ptr bttvLiveUpdates; std::unique_ptr seventvEventAPI; +public: + std::unique_ptr &getBTTVLiveUpdates() override; + std::unique_ptr &getSeventvEventAPI() override; + const IndirectChannel &getWatchingChannel() const override; + void setWatchingChannel(ChannelPtr newWatchingChannel) override; + ChannelPtr getWhispersChannel() const override; + ChannelPtr getMentionsChannel() const override; + ChannelPtr getLiveChannel() const override; + ChannelPtr getAutomodChannel() const override; QString getLastUserThatWhisperedMe() const override; + void setLastUserThatWhisperedMe(const QString &user) override; protected: void initializeConnection(IrcConnection *connection, diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 6150cd8faeb..524913ef8f4 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -236,14 +236,13 @@ void NativeMessagingServer::ReceiverThread::handleSelect( } postToThread([=] { - auto *app = getApp(); - if (!name.isEmpty()) { - auto channel = app->twitch->getOrAddChannel(name); - if (app->twitch->watchingChannel.get() != channel) + auto channel = + getIApp()->getTwitchAbstract()->getOrAddChannel(name); + if (getIApp()->getTwitch()->getWatchingChannel().get() != channel) { - app->twitch->watchingChannel.reset(channel); + getIApp()->getTwitch()->setWatchingChannel(channel); } } @@ -253,7 +252,8 @@ void NativeMessagingServer::ReceiverThread::handleSelect( auto *window = AttachedWindow::getForeground(args); if (!name.isEmpty()) { - window->setChannel(app->twitch->getOrAddChannel(name)); + window->setChannel( + getIApp()->getTwitchAbstract()->getOrAddChannel(name)); } #endif } @@ -294,8 +294,6 @@ void NativeMessagingServer::syncChannels(const QJsonArray &twitchChannels) { assertInGuiThread(); - auto *app = getApp(); - std::vector updated; updated.reserve(twitchChannels.size()); for (const auto &value : twitchChannels) @@ -306,7 +304,8 @@ void NativeMessagingServer::syncChannels(const QJsonArray &twitchChannels) continue; } // the deduping is done on the extension side - updated.emplace_back(app->twitch->getOrAddChannel(name)); + updated.emplace_back( + getIApp()->getTwitchAbstract()->getOrAddChannel(name)); } // This will destroy channels that aren't used anymore. diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index 4eda9e3bd47..5b8b4402b0d 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -74,7 +74,7 @@ bool isBroadcasterSoftwareActive() shouldShowTimeoutWarning = false; postToThread([] { - getApp()->twitch->addGlobalSystemMessage( + getIApp()->getTwitchAbstract()->addGlobalSystemMessage( "Streamer Mode is set to Automatic, but pgrep timed " "out. This can happen if your system lagged at the " "wrong moment. If Streamer Mode continues to not work, " @@ -94,7 +94,7 @@ bool isBroadcasterSoftwareActive() shouldShowWarning = false; postToThread([] { - getApp()->twitch->addGlobalSystemMessage( + getIApp()->getTwitchAbstract()->addGlobalSystemMessage( "Streamer Mode is set to Automatic, but pgrep is " "missing. " "Install it to fix the issue or set Streamer Mode to " diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 69586928883..1e0f7845198 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -688,27 +688,28 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) if (descriptor.type_ == "twitch") { - return app->twitch->getOrAddChannel(descriptor.channelName_); + return getIApp()->getTwitchAbstract()->getOrAddChannel( + descriptor.channelName_); } else if (descriptor.type_ == "mentions") { - return app->twitch->mentionsChannel; + return getIApp()->getTwitch()->getMentionsChannel(); } else if (descriptor.type_ == "watching") { - return app->twitch->watchingChannel; + return getIApp()->getTwitch()->getWatchingChannel(); } else if (descriptor.type_ == "whispers") { - return app->twitch->whispersChannel; + return getIApp()->getTwitch()->getWhispersChannel(); } else if (descriptor.type_ == "live") { - return app->twitch->liveChannel; + return getIApp()->getTwitch()->getLiveChannel(); } else if (descriptor.type_ == "automod") { - return app->twitch->automodChannel; + return getIApp()->getTwitch()->getAutomodChannel(); } else if (descriptor.type_ == "irc") { @@ -717,7 +718,8 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) } else if (descriptor.type_ == "misc") { - return app->twitch->getChannelOrEmpty(descriptor.channelName_); + return getIApp()->getTwitchAbstract()->getChannelOrEmpty( + descriptor.channelName_); } return Channel::getEmpty(); diff --git a/src/widgets/FramelessEmbedWindow.cpp b/src/widgets/FramelessEmbedWindow.cpp index cf61c9bd4be..9735edcee74 100644 --- a/src/widgets/FramelessEmbedWindow.cpp +++ b/src/widgets/FramelessEmbedWindow.cpp @@ -54,7 +54,8 @@ bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType, auto channelName = root.value("channel-name").toString(); this->split_->setChannel( - getApp()->twitch->getOrAddChannel(channelName)); + getIApp()->getTwitchAbstract()->getOrAddChannel( + channelName)); } } } diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index a9b0995ddfb..a1a646c4b6d 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -254,7 +254,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) const auto &messages = getSampleMiscMessages(); static int index = 0; const auto &msg = messages[index++ % messages.size()]; - getApp()->twitch->addFakeMessage(msg); + getIApp()->getTwitchAbstract()->addFakeMessage(msg); return ""; }); @@ -262,7 +262,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) const auto &messages = getSampleCheerMessages(); static int index = 0; const auto &msg = messages[index++ % messages.size()]; - getApp()->twitch->addFakeMessage(msg); + getIApp()->getTwitchAbstract()->addFakeMessage(msg); return ""; }); @@ -270,7 +270,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) const auto &messages = getSampleLinkMessages(); static int index = 0; const auto &msg = messages[index++ % messages.size()]; - getApp()->twitch->addFakeMessage(msg); + getIApp()->getTwitchAbstract()->addFakeMessage(msg); return ""; }); @@ -286,7 +286,8 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) oMessage->toInner() ->toInner(); - app->twitch->addFakeMessage(getSampleChannelRewardIRCMessage()); + getIApp()->getTwitchAbstract()->addFakeMessage( + getSampleChannelRewardIRCMessage()); getIApp()->getTwitchPubSub()->pointReward.redeemed.invoke( oInnerMessage->data.value("redemption").toObject()); alt = !alt; @@ -309,7 +310,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) const auto &messages = getSampleEmoteTestMessages(); static int index = 0; const auto &msg = messages[index++ % messages.size()]; - getApp()->twitch->addFakeMessage(msg); + getIApp()->getTwitchAbstract()->addFakeMessage(msg); return ""; }); @@ -317,7 +318,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) const auto &messages = getSampleSubMessages(); static int index = 0; const auto &msg = messages[index++ % messages.size()]; - getApp()->twitch->addFakeMessage(msg); + getIApp()->getTwitchAbstract()->addFakeMessage(msg); return ""; }); #endif @@ -480,8 +481,8 @@ void Window::addShortcuts() splitContainer = this->notebook_->getOrAddSelectedPage(); } Split *split = new Split(splitContainer); - split->setChannel( - getApp()->twitch->getOrAddChannel(si.channelName)); + split->setChannel(getIApp()->getTwitchAbstract()->getOrAddChannel( + si.channelName)); split->setFilters(si.filters); splitContainer->insertSplit(split); splitContainer->setSelected(split); diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index febd382764f..d482bb12123 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -375,35 +375,33 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const return this->selectedChannel_; } - auto *app = getApp(); - switch (this->ui_.notebook->getSelectedIndex()) { case TAB_TWITCH: { if (this->ui_.twitch.channel->isChecked()) { - return app->twitch->getOrAddChannel( + return getIApp()->getTwitchAbstract()->getOrAddChannel( this->ui_.twitch.channelName->text().trimmed()); } else if (this->ui_.twitch.watching->isChecked()) { - return app->twitch->watchingChannel; + return getIApp()->getTwitch()->getWatchingChannel(); } else if (this->ui_.twitch.mentions->isChecked()) { - return app->twitch->mentionsChannel; + return getIApp()->getTwitch()->getMentionsChannel(); } else if (this->ui_.twitch.whispers->isChecked()) { - return app->twitch->whispersChannel; + return getIApp()->getTwitch()->getWhispersChannel(); } else if (this->ui_.twitch.live->isChecked()) { - return app->twitch->liveChannel; + return getIApp()->getTwitch()->getLiveChannel(); } else if (this->ui_.twitch.automod->isChecked()) { - return app->twitch->automodChannel; + return getIApp()->getTwitch()->getAutomodChannel(); } } break; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index fff4fefc15c..d519cdcfc8d 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -298,21 +298,23 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split) menu->addAction( "Open channel in a new popup window", this, [loginName] { - auto *app = getApp(); + auto *app = getIApp(); auto &window = app->getWindows()->createWindow( WindowType::Popup, true); auto *split = window.getNotebook() .getOrAddSelectedPage() ->appendNewSplit(false); - split->setChannel(app->twitch->getOrAddChannel( - loginName.toLower())); + split->setChannel( + app->getTwitchAbstract()->getOrAddChannel( + loginName.toLower())); }); menu->addAction( "Open channel in a new tab", this, [loginName] { ChannelPtr channel = - getApp()->twitch->getOrAddChannel( - loginName); + getIApp() + ->getTwitchAbstract() + ->getOrAddChannel(loginName); auto &nb = getApp() ->getWindows() ->getMainWindow() diff --git a/src/widgets/dialogs/switcher/NewPopupItem.cpp b/src/widgets/dialogs/switcher/NewPopupItem.cpp index ac4c45a9ccb..d4bc01bbd84 100644 --- a/src/widgets/dialogs/switcher/NewPopupItem.cpp +++ b/src/widgets/dialogs/switcher/NewPopupItem.cpp @@ -21,7 +21,8 @@ NewPopupItem::NewPopupItem(const QString &channelName) void NewPopupItem::action() { - auto channel = getApp()->twitch->getOrAddChannel(this->channelName_); + auto channel = + getIApp()->getTwitchAbstract()->getOrAddChannel(this->channelName_); getIApp()->getWindows()->openInPopup(channel); } diff --git a/src/widgets/dialogs/switcher/NewTabItem.cpp b/src/widgets/dialogs/switcher/NewTabItem.cpp index c264785b254..967c14aca1f 100644 --- a/src/widgets/dialogs/switcher/NewTabItem.cpp +++ b/src/widgets/dialogs/switcher/NewTabItem.cpp @@ -26,7 +26,8 @@ void NewTabItem::action() SplitContainer *container = nb.addPage(true); Split *split = new Split(container); - split->setChannel(getApp()->twitch->getOrAddChannel(this->channelName_)); + split->setChannel( + getIApp()->getTwitchAbstract()->getOrAddChannel(this->channelName_)); container->insertSplit(split); } diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index ed636cca4b6..00469e0a9fe 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1383,17 +1383,20 @@ MessageElementFlags ChannelView::getFlags() const { flags.set(MessageElementFlag::ModeratorTools); } - if (this->underlyingChannel_ == app->twitch->mentionsChannel || - this->underlyingChannel_ == app->twitch->liveChannel || - this->underlyingChannel_ == app->twitch->automodChannel) + if (this->underlyingChannel_ == + getIApp()->getTwitch()->getMentionsChannel() || + this->underlyingChannel_ == + getIApp()->getTwitch()->getLiveChannel() || + this->underlyingChannel_ == + getIApp()->getTwitch()->getAutomodChannel()) { flags.set(MessageElementFlag::ChannelName); flags.unset(MessageElementFlag::ChannelPointReward); } } - if (this->sourceChannel_ == app->twitch->mentionsChannel || - this->sourceChannel_ == app->twitch->automodChannel) + if (this->sourceChannel_ == getIApp()->getTwitch()->getMentionsChannel() || + this->sourceChannel_ == getIApp()->getTwitch()->getAutomodChannel()) { flags.set(MessageElementFlag::ChannelName); } @@ -1546,8 +1549,8 @@ void ChannelView::drawMessages(QPainter &painter, const QRect &area) .canvasWidth = this->width(), .isWindowFocused = this->window() == QApplication::activeWindow(), - .isMentions = - this->underlyingChannel_ == getApp()->twitch->mentionsChannel, + .isMentions = this->underlyingChannel_ == + getIApp()->getTwitch()->getMentionsChannel(), .y = int(-(messagesSnapshot[start]->getHeight() * (fmod(this->scrollBar_->getRelativeCurrentValue(), 1)))), @@ -2707,8 +2710,8 @@ void ChannelView::showUserInfoPopup(const QString &userName, auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup, this->split_); - auto contextChannel = - getApp()->twitch->getChannelOrEmpty(alternativePopoutChannel); + auto contextChannel = getIApp()->getTwitchAbstract()->getChannelOrEmpty( + alternativePopoutChannel); auto openingChannel = this->hasSourceChannel() ? this->sourceChannel_ : this->underlyingChannel_; userPopup->setData(userName, contextChannel, openingChannel); diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index e21e9809e8a..9d27b5d080c 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -570,7 +570,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) // as an official description from 7TV devs is best s.showUnlistedSevenTVEmotes.connect( []() { - getApp()->twitch->forEachChannelAndSpecialChannels( + getIApp()->getTwitch()->forEachChannelAndSpecialChannels( [](const auto &c) { if (c->isTwitchChannel()) { diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 1e34aefbf82..2be5bff5705 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -272,7 +272,7 @@ Split::Split(QWidget *parent) std::ignore = this->view_->openChannelIn.connect( [this](QString twitchChannel, FromTwitchLinkOpenChannelIn openIn) { ChannelPtr channel = - getApp()->twitch->getOrAddChannel(twitchChannel); + getIApp()->getTwitchAbstract()->getOrAddChannel(twitchChannel); switch (openIn) { case FromTwitchLinkOpenChannelIn::Split: From d00cadf4eb0bd6f9221226013fe802d37ba88d00 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 2 Jun 2024 16:31:17 +0200 Subject: [PATCH 075/105] refactor: load images in workers and push immediately (#5431) --- .clang-tidy | 3 +- CHANGELOG.md | 1 + src/messages/Image.cpp | 400 +++++++++++++++++--------------------- src/messages/Image.hpp | 89 +++++---- src/util/PostToThread.hpp | 9 + 5 files changed, 239 insertions(+), 263 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ae07280120b..42cca83f6b7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -30,7 +30,8 @@ Checks: "-*, -readability-function-cognitive-complexity, -bugprone-easily-swappable-parameters, -cert-err58-cpp, - -modernize-avoid-c-arrays + -modernize-avoid-c-arrays, + -misc-include-cleaner " CheckOptions: - key: readability-identifier-naming.ClassCase diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac43b47a39..3a14c431c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Dev: Reduced the amount of scale events. (#5404, #5406) - Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) +- Dev: Images are now loaded in worker threads. (#5431) ## 2.5.1 diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index f39485d5fd8..30706969105 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -21,276 +21,238 @@ #include #include -#include -#include -#include +#include // Duration between each check of every Image instance const auto IMAGE_POOL_CLEANUP_INTERVAL = std::chrono::minutes(1); // Duration since last usage of Image pixmap before expiration of frames const auto IMAGE_POOL_IMAGE_LIFETIME = std::chrono::minutes(10); -namespace chatterino { -namespace detail { - // Frames - Frames::Frames() +namespace chatterino::detail { + +Frames::Frames() +{ + DebugCount::increase("images"); +} + +Frames::Frames(QList &&frames) + : items_(std::move(frames)) +{ + assertInGuiThread(); + DebugCount::increase("images"); + if (!this->empty()) { - DebugCount::increase("images"); + DebugCount::increase("loaded images"); } - Frames::Frames(QVector> &&frames) - : items_(std::move(frames)) + if (this->animated()) { - assertInGuiThread(); - DebugCount::increase("images"); - if (!this->empty()) - { - DebugCount::increase("loaded images"); - } - - if (this->animated()) - { - DebugCount::increase("animated images"); + DebugCount::increase("animated images"); - this->gifTimerConnection_ = - getIApp()->getEmotes()->getGIFTimer().signal.connect([this] { - this->advance(); - }); - } + this->gifTimerConnection_ = + getIApp()->getEmotes()->getGIFTimer().signal.connect([this] { + this->advance(); + }); + } - auto totalLength = - std::accumulate(this->items_.begin(), this->items_.end(), 0UL, - [](auto init, auto &&frame) { - return init + frame.duration; - }); + auto totalLength = std::accumulate(this->items_.begin(), this->items_.end(), + 0UL, [](auto init, auto &&frame) { + return init + frame.duration; + }); - if (totalLength == 0) - { - this->durationOffset_ = 0; - } - else - { - this->durationOffset_ = std::min( - int(getIApp()->getEmotes()->getGIFTimer().position() % - totalLength), - 60000); - } - this->processOffset(); - DebugCount::increase("image bytes", this->memoryUsage()); - DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); + if (totalLength == 0) + { + this->durationOffset_ = 0; } - - Frames::~Frames() + else { - assertInGuiThread(); - DebugCount::decrease("images"); - if (!this->empty()) - { - DebugCount::decrease("loaded images"); - } + this->durationOffset_ = std::min( + int(getIApp()->getEmotes()->getGIFTimer().position() % totalLength), + 60000); + } + this->processOffset(); + DebugCount::increase("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); +} - if (this->animated()) - { - DebugCount::decrease("animated images"); - } - DebugCount::decrease("image bytes", this->memoryUsage()); - DebugCount::increase("image bytes (ever unloaded)", - this->memoryUsage()); +Frames::~Frames() +{ + assertInGuiThread(); + DebugCount::decrease("images"); + if (!this->empty()) + { + DebugCount::decrease("loaded images"); + } - this->gifTimerConnection_.disconnect(); + if (this->animated()) + { + DebugCount::decrease("animated images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage()); + + this->gifTimerConnection_.disconnect(); +} - int64_t Frames::memoryUsage() const +int64_t Frames::memoryUsage() const +{ + int64_t usage = 0; + for (const auto &frame : this->items_) { - int64_t usage = 0; - for (const auto &frame : this->items_) - { - auto sz = frame.image.size(); - auto area = sz.width() * sz.height(); - auto memory = area * frame.image.depth() / 8; + auto sz = frame.image.size(); + auto area = sz.width() * sz.height(); + auto memory = area * frame.image.depth() / 8; - usage += memory; - } - return usage; + usage += memory; } + return usage; +} + +void Frames::advance() +{ + this->durationOffset_ += GIF_FRAME_LENGTH; + this->processOffset(); +} - void Frames::advance() +void Frames::processOffset() +{ + if (this->items_.isEmpty()) { - this->durationOffset_ += GIF_FRAME_LENGTH; - this->processOffset(); + return; } - void Frames::processOffset() + while (true) { - if (this->items_.isEmpty()) - { - return; - } + this->index_ %= this->items_.size(); - while (true) + if (this->durationOffset_ > this->items_[this->index_].duration) { - this->index_ %= this->items_.size(); - - if (this->durationOffset_ > this->items_[this->index_].duration) - { - this->durationOffset_ -= this->items_[this->index_].duration; - this->index_ = (this->index_ + 1) % this->items_.size(); - } - else - { - break; - } + this->durationOffset_ -= this->items_[this->index_].duration; + this->index_ = (this->index_ + 1) % this->items_.size(); } - } - - void Frames::clear() - { - assertInGuiThread(); - if (!this->empty()) + else { - DebugCount::decrease("loaded images"); + break; } - DebugCount::decrease("image bytes", this->memoryUsage()); - DebugCount::increase("image bytes (ever unloaded)", - this->memoryUsage()); - - this->items_.clear(); - this->index_ = 0; - this->durationOffset_ = 0; - this->gifTimerConnection_.disconnect(); } +} - bool Frames::empty() const +void Frames::clear() +{ + assertInGuiThread(); + if (!this->empty()) { - return this->items_.empty(); + DebugCount::decrease("loaded images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage()); + + this->items_.clear(); + this->index_ = 0; + this->durationOffset_ = 0; + this->gifTimerConnection_.disconnect(); +} + +bool Frames::empty() const +{ + return this->items_.empty(); +} + +bool Frames::animated() const +{ + return this->items_.size() > 1; +} - bool Frames::animated() const +std::optional Frames::current() const +{ + if (this->items_.empty()) { - return this->items_.size() > 1; + return std::nullopt; } - std::optional Frames::current() const - { - if (this->items_.empty()) - { - return std::nullopt; - } + return this->items_[this->index_].image; +} - return this->items_[this->index_].image; +std::optional Frames::first() const +{ + if (this->items_.empty()) + { + return std::nullopt; } - std::optional Frames::first() const - { - if (this->items_.empty()) - { - return std::nullopt; - } + return this->items_.front().image; +} - return this->items_.front().image; - } +QList readFrames(QImageReader &reader, const Url &url) +{ + QList frames; + frames.reserve(reader.imageCount()); - // functions - QVector> readFrames(QImageReader &reader, const Url &url) + for (int index = 0; index < reader.imageCount(); ++index) { - QVector> frames; - frames.reserve(reader.imageCount()); - - QImage image; - for (int index = 0; index < reader.imageCount(); ++index) + auto pixmap = QPixmap::fromImageReader(&reader); + if (!pixmap.isNull()) { - if (reader.read(&image)) + // It seems that browsers have special logic for fast animations. + // This implements Chrome and Firefox's behavior which uses + // a duration of 100 ms for any frames that specify a duration of <= 10 ms. + // See http://webkit.org/b/36082 for more information. + // https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231 + int duration = reader.nextImageDelay(); + if (duration <= 10) { - // It seems that browsers have special logic for fast animations. - // This implements Chrome and Firefox's behavior which uses - // a duration of 100 ms for any frames that specify a duration of <= 10 ms. - // See http://webkit.org/b/36082 for more information. - // https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231 - int duration = reader.nextImageDelay(); - if (duration <= 10) - { - duration = 100; - } - duration = std::max(20, duration); - frames.push_back(Frame{std::move(image), duration}); + duration = 100; } + duration = std::max(20, duration); + frames.append(Frame{ + .image = std::move(pixmap), + .duration = duration, + }); } - - if (frames.empty()) - { - qCDebug(chatterinoImage) - << "Error while reading image" << url.string << ": '" - << reader.errorString() << "'"; - } - - return frames; } - // parsed - template - void assignDelayed( - std::queue>>> &queued, - std::mutex &mutex, std::atomic_bool &loadedEventQueued) + if (frames.empty()) { - std::lock_guard lock(mutex); - int i = 0; + qCDebug(chatterinoImage) << "Error while reading image" << url.string + << ": '" << reader.errorString() << "'"; + } - while (!queued.empty()) - { - auto front = std::move(queued.front()); - queued.pop(); + return frames; +} - // Call Assign with the vector of frames - front.first(std::move(front.second)); +void assignFrames(std::weak_ptr weak, QList parsed) +{ + static bool isPushQueued; - if (++i > 50) - { - QTimer::singleShot(3, [&] { - assignDelayed(queued, mutex, loadedEventQueued); - }); - return; - } + auto cb = [parsed = std::move(parsed), weak = std::move(weak)]() mutable { + auto shared = weak.lock(); + if (!shared) + { + return; } + shared->frames_ = std::make_unique(std::move(parsed)); + + // Avoid too many layouts in one event-loop iteration + // + // This callback is called for every image, so there might be multiple + // callbacks queued on the event-loop in this iteration, but we only + // want to generate one invalidation. + if (!isPushQueued) + { + isPushQueued = true; + postToThread([] { + isPushQueued = false; + getIApp()->getWindows()->forceLayoutChannelViews(); + }); + } + }; - getIApp()->getWindows()->forceLayoutChannelViews(); + postToGuiThread(cb); +} - loadedEventQueued = false; - } +} // namespace chatterino::detail - template - auto makeConvertCallback(const QVector> &parsed, - Assign assign) - { - static std::queue>>> queued; - static std::mutex mutex; - static std::atomic_bool loadedEventQueued{false}; - - return [parsed, assign] { - // convert to pixmap - QVector> frames; - frames.reserve(parsed.size()); - std::transform(parsed.begin(), parsed.end(), - std::back_inserter(frames), [](auto &frame) { - return Frame{ - QPixmap::fromImage(frame.image), - frame.duration}; - }); - - // put into stack - std::lock_guard lock(mutex); - queued.emplace(assign, frames); - - if (!loadedEventQueued) - { - loadedEventQueued = true; - - QTimer::singleShot(100, [=] { - assignDelayed(queued, mutex, loadedEventQueued); - }); - } - }; - } -} // namespace detail +namespace chatterino { // IMAGE2 Image::~Image() @@ -402,7 +364,7 @@ void Image::setPixmap(const QPixmap &pixmap) { auto setFrames = [shared = this->shared_from_this(), pixmap]() { shared->frames_ = std::make_unique( - QVector>{detail::Frame{pixmap, 1}}); + QList{detail::Frame{pixmap, 1}}); }; if (isGuiThread()) @@ -512,11 +474,8 @@ void Image::actuallyLoad() return; } - auto data = result.getData(); - - // const cast since we are only reading from it - QBuffer buffer(const_cast(&data)); - buffer.open(QIODevice::ReadOnly); + QBuffer buffer; + buffer.setData(result.getData()); QImageReader reader(&buffer); if (!reader.canRead()) @@ -557,14 +516,7 @@ void Image::actuallyLoad() auto parsed = detail::readFrames(reader, shared->url()); - postToThread(makeConvertCallback( - parsed, [weak = std::weak_ptr(shared)](auto &&frames) { - if (auto shared = weak.lock()) - { - shared->frames_ = std::make_unique( - std::forward(frames)); - } - })); + assignFrames(shared, parsed); }) .onError([weak](auto /*result*/) { auto shared = weak.lock(); diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 2eb0fcf04ca..31351ab78dc 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -1,15 +1,14 @@ #pragma once #include "common/Aliases.hpp" -#include "common/Common.hpp" #include #include +#include #include #include #include #include -#include #include #include @@ -19,41 +18,53 @@ #include namespace chatterino { -namespace detail { - template - struct Frame { - Image image; - int duration; - }; - class Frames - { - public: - Frames(); - Frames(QVector> &&frames); - ~Frames(); - - Frames(const Frames &) = delete; - Frames &operator=(const Frames &) = delete; - - Frames(Frames &&) = delete; - Frames &operator=(Frames &&) = delete; - - void clear(); - bool empty() const; - bool animated() const; - void advance(); - std::optional current() const; - std::optional first() const; - - private: - int64_t memoryUsage() const; - void processOffset(); - QVector> items_; - int index_{0}; - int durationOffset_{0}; - pajlada::Signals::Connection gifTimerConnection_; - }; -} // namespace detail + +class Image; + +} // namespace chatterino + +namespace chatterino::detail { + +struct Frame { + QPixmap image; + int duration; +}; + +class Frames +{ +public: + Frames(); + Frames(QList &&frames); + ~Frames(); + + Frames(const Frames &) = delete; + Frames &operator=(const Frames &) = delete; + + Frames(Frames &&) = delete; + Frames &operator=(Frames &&) = delete; + + void clear(); + bool empty() const; + bool animated() const; + void advance(); + std::optional current() const; + std::optional first() const; + +private: + int64_t memoryUsage() const; + void processOffset(); + QList items_; + QList::size_type index_{0}; + int durationOffset_{0}; + pajlada::Signals::Connection gifTimerConnection_; +}; + +QList readFrames(QImageReader &reader, const Url &url); +void assignFrames(std::weak_ptr weak, QList parsed); + +} // namespace chatterino::detail + +namespace chatterino { class Image; using ImagePtr = std::shared_ptr; @@ -116,9 +127,11 @@ class Image : public std::enable_shared_from_this mutable std::chrono::time_point lastUsed_; // gui thread only - std::unique_ptr frames_{}; + std::unique_ptr frames_; friend class ImageExpirationPool; + friend void detail::assignFrames(std::weak_ptr, + QList); }; // forward-declarable function that calls Image::getEmpty() under the hood. diff --git a/src/util/PostToThread.hpp b/src/util/PostToThread.hpp index afeb34d06b0..e0db1fdd075 100644 --- a/src/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -70,4 +70,13 @@ static void runInGuiThread(F &&fun) } } +template +inline void postToGuiThread(F &&fun) +{ + assert(!isGuiThread() && + "postToGuiThread must be called from a non-GUI thread"); + + postToThread(std::forward(fun)); +} + } // namespace chatterino From 248cd46eb7cc3954b6cabed1769edf563ef24879 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 3 Jun 2024 10:31:30 +0200 Subject: [PATCH 076/105] fix: global emotes not loading (#5435) * fix: manually initialize twitchircserver after rest of singletons are initialized this fixes global emotes not being loaded on startup, since initialize was never called (since it was no longer added to the singleton list) * unrelated nit: remove copy/move ctors/operators of twitchircserver --- CHANGELOG.md | 2 +- src/Application.cpp | 2 ++ src/providers/twitch/TwitchIrcServer.cpp | 2 +- src/providers/twitch/TwitchIrcServer.hpp | 12 +++++++----- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a14c431c84..1458dd4844b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334, #5393) -- Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421) +- Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421, #5435) - Dev: Reduced the amount of scale events. (#5404, #5406) - Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) diff --git a/src/Application.cpp b/src/Application.cpp index 5d70316bf1d..f9250f78121 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -211,6 +211,8 @@ void Application::initialize(Settings &settings, const Paths &paths) singleton->initialize(settings, paths); } + this->twitch->initialize(); + // XXX: Loading Twitch badges after Helix has been initialized, which only happens after // the AccountController initialize has been called this->twitchBadges->loadTwitchBadges(); diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index e318d45af19..7012cdb2192 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -165,7 +165,7 @@ TwitchIrcServer::TwitchIrcServer() // false); } -void TwitchIrcServer::initialize(Settings &settings, const Paths &paths) +void TwitchIrcServer::initialize() { getIApp()->getAccounts()->twitch.currentUserChanged.connect([this]() { postToThread([this] { diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 15e3af990bc..1f3dbe73028 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -2,7 +2,6 @@ #include "common/Atomic.hpp" #include "common/Channel.hpp" -#include "common/Singleton.hpp" #include "providers/irc/AbstractIrcServer.hpp" #include @@ -52,15 +51,18 @@ class ITwitchIrcServer // Update this interface with TwitchIrcServer methods as needed }; -class TwitchIrcServer final : public AbstractIrcServer, - public Singleton, - public ITwitchIrcServer +class TwitchIrcServer final : public AbstractIrcServer, public ITwitchIrcServer { public: TwitchIrcServer(); ~TwitchIrcServer() override = default; - void initialize(Settings &settings, const Paths &paths) override; + TwitchIrcServer(const TwitchIrcServer &) = delete; + TwitchIrcServer(TwitchIrcServer &&) = delete; + TwitchIrcServer &operator=(const TwitchIrcServer &) = delete; + TwitchIrcServer &operator=(TwitchIrcServer &&) = delete; + + void initialize(); void forEachChannelAndSpecialChannels( std::function func) override; From d2316af70f3bb2bee77fb642ffbb048806321a08 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Thu, 6 Jun 2024 05:13:13 -0500 Subject: [PATCH 077/105] feat: notify mods when users are warned (#5441) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/Application.cpp | 18 +++++++++++++ src/messages/MessageBuilder.cpp | 25 ++++++++++++++++++ src/messages/MessageBuilder.hpp | 2 ++ src/providers/twitch/PubSubActions.hpp | 9 +++++++ src/providers/twitch/PubSubManager.cpp | 35 ++++++++++++++++++++++++-- src/providers/twitch/PubSubManager.hpp | 2 ++ 7 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1458dd4844b..7fc6cf37324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Minor: The size of the emote popup is now saved. (#5415) - Minor: Added the ability to duplicate tabs. (#5277) +- Minor: Moderators can now see when users are warned. (#5441) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/Application.cpp b/src/Application.cpp index f9250f78121..77663923f88 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -650,6 +650,24 @@ void Application::initPubSub() chan->addOrReplaceTimeout(msg.release()); }); }); + + std::ignore = this->twitchPubSub->moderation.userWarned.connect( + [&](const auto &action) { + auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + // TODO: Resolve the moderator's user ID into a full user here, so message can look better + postToThread([chan, action] { + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); + chan->addMessage(msg.release()); + }); + }); + std::ignore = this->twitchPubSub->moderation.messageDeleted.connect( [&](const auto &action) { auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index cccd540d023..ef0cb8c1c1b 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -323,6 +323,31 @@ MessageBuilder::MessageBuilder(const UnbanAction &action) this->message().searchText = text; } +MessageBuilder::MessageBuilder(const WarnAction &action) + : MessageBuilder() +{ + this->emplace(); + this->message().flags.set(MessageFlag::System); + + QString text; + + // TODO: Use MentionElement here, once WarnAction includes username/displayname + this->emplaceSystemTextAndUpdate("A moderator", text) + ->setLink({Link::UserInfo, "id:" + action.source.id}); + this->emplaceSystemTextAndUpdate("warned", text); + this->emplaceSystemTextAndUpdate( + action.target.login + (action.reasons.isEmpty() ? "." : ":"), text) + ->setLink({Link::UserInfo, action.target.login}); + + if (!action.reasons.isEmpty()) + { + this->emplaceSystemTextAndUpdate(action.reasons.join(", "), text); + } + + this->message().messageText = text; + this->message().searchText = text; +} + MessageBuilder::MessageBuilder(const AutomodUserAction &action) : MessageBuilder() { diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index c7277997a27..63b9fd8cb95 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -12,6 +12,7 @@ namespace chatterino { struct BanAction; struct UnbanAction; +struct WarnAction; struct AutomodAction; struct AutomodUserAction; struct AutomodInfoAction; @@ -78,6 +79,7 @@ class MessageBuilder const QTime &time = QTime::currentTime()); MessageBuilder(const BanAction &action, uint32_t count = 1); MessageBuilder(const UnbanAction &action); + MessageBuilder(const WarnAction &action); MessageBuilder(const AutomodUserAction &action); MessageBuilder(LiveUpdatesAddEmoteMessageTag, const QString &platform, diff --git a/src/providers/twitch/PubSubActions.hpp b/src/providers/twitch/PubSubActions.hpp index 89abdb24464..82c94da3004 100644 --- a/src/providers/twitch/PubSubActions.hpp +++ b/src/providers/twitch/PubSubActions.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -171,4 +172,12 @@ struct AutomodInfoAction : PubSubAction { } type; }; +struct WarnAction : PubSubAction { + using PubSubAction::PubSubAction; + + ActionUser target; + + QStringList reasons; +}; + } // namespace chatterino diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index acb75cb59ba..cefde91696a 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -286,6 +286,37 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) this->moderation.userUnbanned.invoke(action); }; + this->moderationActionHandlers["warn"] = [this](const auto &data, + const auto &roomID) { + WarnAction action(data, roomID); + + action.source.id = data.value("created_by_user_id").toString(); + action.source.login = + data.value("created_by").toString(); // currently always empty + + action.target.id = data.value("target_user_id").toString(); + action.target.login = data.value("target_user_login").toString(); + + const auto reasons = data.value("args").toArray(); + bool firstArg = true; + for (const auto &reasonValue : reasons) + { + if (firstArg) + { + // Skip first arg in the reasons array since it's not a reason + firstArg = false; + continue; + } + const auto &reason = reasonValue.toString(); + if (!reason.isEmpty()) + { + action.reasons.append(reason); + } + } + + this->moderation.userWarned.invoke(action); + }; + /* // This handler is no longer required as we use the automod-queue topic now this->moderationActionHandlers["automod_rejected"] = @@ -1131,8 +1162,8 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) case PubSubChatModeratorActionMessage::Type::INVALID: default: { - qCDebug(chatterinoPubSub) - << "Invalid whisper type:" << innerMessage.typeString; + qCDebug(chatterinoPubSub) << "Invalid moderator action type:" + << innerMessage.typeString; } break; } diff --git a/src/providers/twitch/PubSubManager.hpp b/src/providers/twitch/PubSubManager.hpp index 6ddd98369aa..eb27e32f8ee 100644 --- a/src/providers/twitch/PubSubManager.hpp +++ b/src/providers/twitch/PubSubManager.hpp @@ -34,6 +34,7 @@ struct PubSubAutoModQueueMessage; struct AutomodAction; struct AutomodUserAction; struct AutomodInfoAction; +struct WarnAction; struct PubSubLowTrustUsersMessage; struct PubSubWhisperMessage; @@ -97,6 +98,7 @@ class PubSub Signal userBanned; Signal userUnbanned; + Signal userWarned; Signal suspiciousMessageReceived; Signal suspiciousTreatmentUpdated; From b81a94713403dd5eb9a907dc6cb1ac2dcecbb351 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 9 Jun 2024 12:37:18 +0200 Subject: [PATCH 078/105] force install newer version of the windows CRT in the windows test runner (#5448) Co-authored-by: Nerixyz --- .github/workflows/test-windows.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ac1f1c9a079..052c7d81e05 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -105,6 +105,12 @@ jobs: --output-folder=. ` -o with_openssl3="$Env:C2_USE_OPENSSL3" + # The Windows runners currently use an older version of the CRT + - name: Install CRT + run: | + mkdir -Force build-test/bin + cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin + - name: Build run: | cmake ` From 25284fc7034f143966b98917ff4a31f0e540b1e0 Mon Sep 17 00:00:00 2001 From: JakeRYW Date: Sun, 9 Jun 2024 07:15:25 -0400 Subject: [PATCH 079/105] Improved error messaging for Update Channel API (#5429) --- CHANGELOG.md | 1 + mocks/include/mocks/Helix.hpp | 12 ++-- .../commands/builtin/twitch/UpdateChannel.cpp | 70 ++++++++++++++++--- src/providers/twitch/api/Helix.cpp | 66 +++++++++++++++-- src/providers/twitch/api/Helix.hpp | 18 ++++- 5 files changed, 145 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc6cf37324..4a93c4a0ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) - Minor: The size of the emote popup is now saved. (#5415) - Minor: Added the ability to duplicate tabs. (#5277) +- Minor: Improved error messages for channel update commands. (#5429) - Minor: Moderators can now see when users are warned. (#5441) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp index f53f62dda6d..0f0c4110007 100644 --- a/mocks/include/mocks/Helix.hpp +++ b/mocks/include/mocks/Helix.hpp @@ -119,12 +119,12 @@ class Helix : public IHelix HelixFailureCallback failureCallback), (override)); - MOCK_METHOD(void, updateChannel, - (QString broadcasterId, QString gameId, QString language, - QString title, - std::function successCallback, - HelixFailureCallback failureCallback), - (override)); + MOCK_METHOD( + void, updateChannel, + (QString broadcasterId, QString gameId, QString language, QString title, + std::function successCallback, + (FailureCallback failureCallback)), + (override)); MOCK_METHOD(void, manageAutoModMessages, (QString userID, QString msgID, QString action, diff --git a/src/controllers/commands/builtin/twitch/UpdateChannel.cpp b/src/controllers/commands/builtin/twitch/UpdateChannel.cpp index bcda86f384d..bb026bf4d50 100644 --- a/src/controllers/commands/builtin/twitch/UpdateChannel.cpp +++ b/src/controllers/commands/builtin/twitch/UpdateChannel.cpp @@ -7,6 +7,58 @@ #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchChannel.hpp" +namespace { + +using namespace chatterino; + +QString formatUpdateChannelError(const char *updateType, + HelixUpdateChannelError error, + const QString &message) +{ + using Error = HelixUpdateChannelError; + + QString errorMessage = QString("Failed to set %1 - ").arg(updateType); + + switch (error) + { + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += QString("You must be the broadcaster " + "to set the %1.") + .arg(updateType); + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += + QString("An unknown error has occurred (%1).").arg(message); + } + break; + } + + return errorMessage; +} + +} // namespace + namespace chatterino::commands { QString setTitle(const CommandContext &ctx) @@ -30,8 +82,8 @@ QString setTitle(const CommandContext &ctx) return ""; } - auto status = ctx.twitchChannel->accessStreamStatus(); auto title = ctx.words.mid(1).join(" "); + getHelix()->updateChannel( ctx.twitchChannel->roomId(), "", "", title, [channel{ctx.channel}, title](const auto &result) { @@ -40,10 +92,10 @@ QString setTitle(const CommandContext &ctx) channel->addMessage( makeSystemMessage(QString("Updated title to %1").arg(title))); }, - [channel{ctx.channel}] { - channel->addMessage( - makeSystemMessage("Title update failed! Are you " - "missing the required scope?")); + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = + formatUpdateChannelError("title", error, message); + channel->addMessage(makeSystemMessage(errorMessage)); }); return ""; @@ -105,10 +157,10 @@ QString setGame(const CommandContext &ctx) channel->addMessage(makeSystemMessage( QString("Updated game to %1").arg(matchedGame.name))); }, - [channel] { - channel->addMessage( - makeSystemMessage("Game update failed! Are you " - "missing the required scope?")); + [channel](auto error, auto message) { + auto errorMessage = + formatUpdateChannelError("game", error, message); + channel->addMessage(makeSystemMessage(errorMessage)); }); }, [channel{ctx.channel}] { diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index daf17021f0c..460acfdb6ea 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -613,11 +613,13 @@ void Helix::unblockUser(QString targetUserId, const QObject *caller, .execute(); } -void Helix::updateChannel(QString broadcasterId, QString gameId, - QString language, QString title, - std::function successCallback, - HelixFailureCallback failureCallback) +void Helix::updateChannel( + QString broadcasterId, QString gameId, QString language, QString title, + std::function successCallback, + FailureCallback failureCallback) { + using Error = HelixUpdateChannelError; + QUrlQuery urlQuery; auto obj = QJsonObject(); if (!gameId.isEmpty()) @@ -646,7 +648,61 @@ void Helix::updateChannel(QString broadcasterId, QString gameId, successCallback(result); }) .onError([failureCallback](NetworkResult result) { - failureCallback(); + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 400: + case 403: { + failureCallback(Error::Forwarded, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + case 500: { + failureCallback(Error::Unknown, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Helix update channel, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } }) .execute(); } diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 0d5412ba3f0..c24e44bd3e4 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -595,6 +595,19 @@ enum class HelixUpdateChatSettingsError { // update chat settings Forwarded, }; // update chat settings +/// Error type for Helix::updateChannel +/// +/// Used in the /settitle and /setgame commands +enum class HelixUpdateChannelError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + enum class HelixBanUserError { // /timeout, /ban Unknown, UserMissingScope, @@ -862,7 +875,7 @@ class IHelix virtual void updateChannel( QString broadcasterId, QString gameId, QString language, QString title, std::function successCallback, - HelixFailureCallback failureCallback) = 0; + FailureCallback failureCallback) = 0; // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages virtual void manageAutoModMessages( @@ -1183,7 +1196,8 @@ class Helix final : public IHelix void updateChannel(QString broadcasterId, QString gameId, QString language, QString title, std::function successCallback, - HelixFailureCallback failureCallback) final; + FailureCallback + failureCallback) final; // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages void manageAutoModMessages( From c1b2114e613ea34eb870686f6d13e309d6eac595 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:27:11 +0200 Subject: [PATCH 080/105] chore(deps): bump dawidd6/action-download-artifact from 3 to 6 (#5454) Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 3 to 6. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v3...v6) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/create-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create-installer.yml b/.github/workflows/create-installer.yml index ac060c96d3a..5700e3f1682 100644 --- a/.github/workflows/create-installer.yml +++ b/.github/workflows/create-installer.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 # allows for tags access - name: Download artifact - uses: dawidd6/action-download-artifact@v3 + uses: dawidd6/action-download-artifact@v6 with: workflow: build.yml name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip From 66471075a55349ccd03e3dcf081cd6343be00fde Mon Sep 17 00:00:00 2001 From: Jacob Nielsen <3404570+Niller2005@users.noreply.github.com> Date: Sat, 15 Jun 2024 12:04:51 +0200 Subject: [PATCH 081/105] fix: Add brave & google-chrome-stable to incognito browser list (#5452) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/util/IncognitoBrowser.cpp | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a93c4a0ac1..40ac9b928fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Minor: Added the ability to duplicate tabs. (#5277) - Minor: Improved error messages for channel update commands. (#5429) - Minor: Moderators can now see when users are warned. (#5441) +- Minor: Added support for Brave & google-chrome-stable browsers. (#5452) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 93ae2983bb4..77f14160a8b 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -16,12 +16,20 @@ QString getPrivateSwitch(const QString &browserExecutable) { // list of command line switches to turn on private browsing in browsers static auto switches = std::vector>{ - {"firefox", "-private-window"}, {"librewolf", "-private-window"}, - {"waterfox", "-private-window"}, {"icecat", "-private-window"}, - {"chrome", "-incognito"}, {"vivaldi", "-incognito"}, - {"opera", "-newprivatetab"}, {"opera\\launcher", "--private"}, - {"iexplore", "-private"}, {"msedge", "-inprivate"}, - {"firefox-esr", "-private-window"}, {"chromium", "-incognito"}, + {"firefox", "-private-window"}, + {"librewolf", "-private-window"}, + {"waterfox", "-private-window"}, + {"icecat", "-private-window"}, + {"chrome", "-incognito"}, + {"google-chrome-stable", "-incognito"}, + {"vivaldi", "-incognito"}, + {"opera", "-newprivatetab"}, + {"opera\\launcher", "--private"}, + {"iexplore", "-private"}, + {"msedge", "-inprivate"}, + {"firefox-esr", "-private-window"}, + {"chromium", "-incognito"}, + {"brave", "-incognito"}, }; // compare case-insensitively From 280ac302892e28118b8854d04ab109b3c8bb1d9d Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 15 Jun 2024 12:32:01 +0200 Subject: [PATCH 082/105] fix: handle older VC redist versions (#5447) --- .CI/build-installer.ps1 | 2 +- .CI/chatterino-installer.iss | 6 +++--- .CI/deploy-crt.ps1 | 30 ++++++++++++++++++++++++++++++ .github/workflows/build.yml | 1 + CHANGELOG.md | 1 + src/CMakeLists.txt | 1 + 6 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .CI/deploy-crt.ps1 diff --git a/.CI/build-installer.ps1 b/.CI/build-installer.ps1 index 756a1503f29..b60145d82e9 100644 --- a/.CI/build-installer.ps1 +++ b/.CI/build-installer.ps1 @@ -42,7 +42,7 @@ $VCRTVersion = (Get-Item "$Env:VCToolsRedistDir\vc_redist.x64.exe").VersionInfo; ISCC ` /DWORKING_DIR="$($pwd.Path)\" ` /DINSTALLER_BASE_NAME="$installerBaseName" ` - /DSHIPPED_VCRT_BUILD="$($VCRTVersion.FileBuildPart)" ` + /DSHIPPED_VCRT_MINOR="$($VCRTVersion.FileMinorPart)" ` /DSHIPPED_VCRT_VERSION="$($VCRTVersion.FileDescription)" ` $defines ` /O. ` diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss index 1f9816a2978..5068a3c2967 100644 --- a/.CI/chatterino-installer.iss +++ b/.CI/chatterino-installer.iss @@ -120,15 +120,15 @@ begin Result := VCRTVersion + ' is installed'; end; -// Checks if a new VCRT is needed by comparing the builds. +// Checks if a new VCRT is needed by comparing the minor version (the major one is locked at 14). function NeedsNewVCRT(): Boolean; var VCRTBuild: Cardinal; begin Result := True; - if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Bld', VCRTBuild) then + if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Minor', VCRTBuild) then begin - if VCRTBuild >= {#SHIPPED_VCRT_BUILD} then + if VCRTBuild >= {#SHIPPED_VCRT_MINOR} then Result := False; end; end; diff --git a/.CI/deploy-crt.ps1 b/.CI/deploy-crt.ps1 new file mode 100644 index 00000000000..cab12693ef2 --- /dev/null +++ b/.CI/deploy-crt.ps1 @@ -0,0 +1,30 @@ +param ( + [string] $InstallDir = "Chatterino2" +) + +if ($null -eq $Env:VCToolsRedistDir) { + Write-Error "VCToolsRedistDir is not set. Forgot to set Visual Studio environment variables?"; + exit 1 +} + +# A path to the runtime libraries (e.g. "$Env:VCToolsRedistDir\onecore\x64\Microsoft.VC143.CRT") +$vclibs = (Get-ChildItem "$Env:VCToolsRedistDir\onecore\x64" -Filter '*.CRT')[0].FullName; + +# All executables and libraries in the installation directory +$targets = Get-ChildItem -Recurse -Include '*.dll', '*.exe' $InstallDir; +# All dependencies of the targets (with duplicates) +$all_deps = $targets | ForEach-Object { (dumpbin /DEPENDENTS $_.FullName) -match '^(?!Dump of).+\.dll$' } | ForEach-Object { $_.Trim() }; +# All dependencies without duplicates +$dependencies = $all_deps | Sort-Object -Unique; + +$n_deployed = 0; +foreach ($dll in $dependencies) { + Write-Output "Checking for $dll"; + if (Test-Path -PathType Leaf "$vclibs\$dll") { + Write-Output "Deploying $dll"; + Copy-Item "$vclibs\$dll" "$InstallDir\$dll" -Force; + $n_deployed++; + } +} + +Write-Output "Deployed $n_deployed libraries"; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c45b0165beb..a28072ddddd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -251,6 +251,7 @@ jobs: cd build windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ cp bin/chatterino.exe Chatterino2/ + ..\.CI\deploy-crt.ps1 Chatterino2 echo nightly > Chatterino2/modes - name: Package (windows) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ac9b928fe..77753dbaf82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) - Dev: Images are now loaded in worker threads. (#5431) +- Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) ## 2.5.1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 15806aae102..72fa4851ad7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -865,6 +865,7 @@ if (BUILD_APP) endif() get_filename_component(QT_BIN_DIR ${QT_CORE_LOC} DIRECTORY) + # This assumes the installed CRT is up-to-date (see .CI/deploy-crt.ps1) set(WINDEPLOYQT_COMMAND_ARGV "${WINDEPLOYQT_PATH}" "$" ${WINDEPLOYQT_MODE} --no-compiler-runtime --no-translations --no-opengl-sw) string(REPLACE ";" " " WINDEPLOYQT_COMMAND "${WINDEPLOYQT_COMMAND_ARGV}") From 538bead45be86675e23aaa9337d9ec1bc9e99917 Mon Sep 17 00:00:00 2001 From: hemirt <1310440+hemirt@users.noreply.github.com> Date: Sat, 15 Jun 2024 12:59:13 +0200 Subject: [PATCH 083/105] Clean up QtCreator's package manager setup (#5305) --- CHANGELOG.md | 1 + QtCreatorPackageManager.cmake | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 QtCreatorPackageManager.cmake diff --git a/CHANGELOG.md b/CHANGELOG.md index 77753dbaf82..8c2f0242e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Dev: Removed unused timegate settings. (#5361) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) - Dev: Images are now loaded in worker threads. (#5431) +- Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) - Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) ## 2.5.1 diff --git a/QtCreatorPackageManager.cmake b/QtCreatorPackageManager.cmake new file mode 100644 index 00000000000..f26a970c81e --- /dev/null +++ b/QtCreatorPackageManager.cmake @@ -0,0 +1,5 @@ +# https://www.qt.io/blog/qt-creator-cmake-package-manager-auto-setup + +# set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON) # skip both conan and vcpkg auto-setups +# set(QT_CREATOR_SKIP_CONAN_SETUP ON) # skip conan auto-setup +set(QT_CREATOR_SKIP_VCPKG_SETUP ON) # skip vcpkg auto-setup From 86871eec5a96d9e5f8e088de30e8cd41eef5f63c Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 16 Jun 2024 11:34:00 +0200 Subject: [PATCH 084/105] fix: ignore late pings (#5457) --- CHANGELOG.md | 1 + src/providers/irc/IrcConnection2.cpp | 25 ++++++++++++++++++++++++- src/providers/irc/IrcConnection2.hpp | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2f0242e48..e790eb91233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) +- Bugfix: Fixed message history occasionally not loading after a sleep. (#5457) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Use Qt's high DPI scaling. (#4868, #5400) diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 9f97e6db641..19f327c24d0 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -3,6 +3,10 @@ #include "common/QLogging.hpp" #include "common/Version.hpp" +#include + +using namespace std::chrono_literals; + namespace chatterino { namespace { @@ -56,6 +60,7 @@ IrcConnection::IrcConnection(QObject *parent) // Send ping every x seconds this->pingTimer_.setInterval(5000); this->pingTimer_.start(); + this->lastPing_ = std::chrono::system_clock::now(); QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] { if (this->isConnected()) { @@ -64,7 +69,25 @@ IrcConnection::IrcConnection(QObject *parent) // If we're still receiving messages, all is well this->recentlyReceivedMessage_ = false; this->waitingForPong_ = false; - this->heartbeat.invoke(); + + // Check if we got invoked too late (e.g. due to a sleep) + auto now = std::chrono::system_clock::now(); + auto elapsed = now - this->lastPing_; + if (elapsed < 3 * 5000ms) + { + this->heartbeat.invoke(); + } + else + { + qCDebug(chatterinoIrc).nospace() + << "Got late ping (skipping heartbeat): " + << std::chrono::duration_cast< + std::chrono::milliseconds>(elapsed) + .count() + << "ms"; + } + this->lastPing_ = now; + return; } diff --git a/src/providers/irc/IrcConnection2.hpp b/src/providers/irc/IrcConnection2.hpp index 150793ec8a7..0e269397c4c 100644 --- a/src/providers/irc/IrcConnection2.hpp +++ b/src/providers/irc/IrcConnection2.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace chatterino { class IrcConnection : public Communi::IrcConnection @@ -33,6 +35,7 @@ class IrcConnection : public Communi::IrcConnection QTimer pingTimer_; QTimer reconnectTimer_; std::atomic recentlyReceivedMessage_{true}; + std::chrono::time_point lastPing_; // Reconnect with a base delay of 1 second and max out at 1 second * (2^(5-1)) (i.e. 16 seconds) ExponentialBackoff<5> reconnectBackoff_{std::chrono::milliseconds{1000}}; From 9b31246502df1be0f70bf3ce8dd25e6dd080c8f2 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 12:22:51 +0200 Subject: [PATCH 085/105] feat: allow timeout-related commands to be used in multiple channels (#5402) This changes the behaviour of the following commands: - `/ban` - `/timeout` - `/untimeout` - `/unban` All of those commands now accept one or more `--channel` parameters to override which channel the action should take place in. The `--channel` parameter accepts a channel ID or channel name with the same syntax as the other "user targets" do (e.g. `id:11148817` or `pajlada`) examples Ban user in the chat you're typing in: `/ban weeb123` Ban user in the chat you're typing in, with a reason specified: `/ban weeb123 the ban reason` Ban user in a separate chat, with a reason specified: `/ban --channel pajlada weeb123 the ban reason` Ban user in two separate chats, with a reason specified: `/ban --channel pajlada --channel id:117166826 weeb123 the ban reason` Timeout user in the chat you're typing in: `/timeout weeb123` Timeout user in the chat you're typing in, with a reason specified: `/timeout weeb123 10m the timeout reason` Timeout user in a separate chat, with a reason specified: `/timeout --channel pajlada weeb123 10m the timeout reason` Timeout user in two separate chats, with a reason specified: `/timeout --channel pajlada --channel id:117166826 weeb123 10m the timeout reason` Unban user in the chat you're typing in: `/unban weeb123` Unban user in a separate chat: `/unban --channel pajlada weeb123` Unban user in two separate chats: `/unban --channel pajlada --channel id:117166826 weeb123` --- .gitmodules | 3 + CHANGELOG.md | 1 + benchmarks/src/RecentMessages.cpp | 7 + lib/expected-lite | 1 + mocks/include/mocks/EmptyApplication.hpp | 7 +- mocks/include/mocks/LinkResolver.hpp | 32 + mocks/include/mocks/Logging.hpp | 36 + resources/licenses/expected-lite.txt | 23 + src/Application.cpp | 2 +- src/Application.hpp | 12 +- src/CMakeLists.txt | 5 + src/common/QLogging.cpp | 1 + src/common/QLogging.hpp | 3 +- .../commands/builtin/twitch/Ban.cpp | 288 +++-- .../commands/builtin/twitch/Unban.cpp | 140 ++- .../commands/common/ChannelAction.cpp | 184 +++ .../commands/common/ChannelAction.hpp | 59 + src/providers/twitch/TwitchChannel.cpp | 25 +- src/providers/twitch/TwitchChannel.hpp | 1 + src/singletons/Logging.hpp | 13 +- src/widgets/settingspages/AboutPage.cpp | 3 + tests/CMakeLists.txt | 1 + tests/src/Commands.cpp | 1057 +++++++++++++++++ 23 files changed, 1731 insertions(+), 173 deletions(-) create mode 160000 lib/expected-lite create mode 100644 mocks/include/mocks/LinkResolver.hpp create mode 100644 mocks/include/mocks/Logging.hpp create mode 100644 resources/licenses/expected-lite.txt create mode 100644 src/controllers/commands/common/ChannelAction.cpp create mode 100644 src/controllers/commands/common/ChannelAction.hpp create mode 100644 tests/src/Commands.cpp diff --git a/.gitmodules b/.gitmodules index cb1235a8582..e58a5bbd49b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,6 @@ [submodule "tools/crash-handler"] path = tools/crash-handler url = https://github.com/Chatterino/crash-handler +[submodule "lib/expected-lite"] + path = lib/expected-lite + url = https://github.com/martinmoene/expected-lite diff --git a/CHANGELOG.md b/CHANGELOG.md index e790eb91233..e49cea9ba65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) +- Minor: Added the ability for `/ban`, `/timeout`, `/unban`, and `/untimeout` to specify multiple channels to duplicate the action to. Example: `/timeout --channel id:11148817 --channel testaccount_420 forsen 7m game complaining`. (#5402) - Minor: The size of the emote popup is now saved. (#5415) - Minor: Added the ability to duplicate tabs. (#5277) - Minor: Improved error messages for channel update commands. (#5429) diff --git a/benchmarks/src/RecentMessages.cpp b/benchmarks/src/RecentMessages.cpp index fd5fe0f1a14..7f2c0c7aca5 100644 --- a/benchmarks/src/RecentMessages.cpp +++ b/benchmarks/src/RecentMessages.cpp @@ -4,6 +4,7 @@ #include "messages/Emote.hpp" #include "mocks/DisabledStreamerMode.hpp" #include "mocks/EmptyApplication.hpp" +#include "mocks/LinkResolver.hpp" #include "mocks/TwitchIrcServer.hpp" #include "mocks/UserData.hpp" #include "providers/bttv/BttvEmotes.hpp" @@ -99,10 +100,16 @@ class MockApplication : mock::EmptyApplication return &this->streamerMode; } + ILinkResolver *getLinkResolver() override + { + return &this->linkResolver; + } + AccountController accounts; Emotes emotes; mock::UserDataController userData; mock::MockTwitchIrcServer twitch; + mock::EmptyLinkResolver linkResolver; ChatterinoBadges chatterinoBadges; FfzBadges ffzBadges; SeventvBadges seventvBadges; diff --git a/lib/expected-lite b/lib/expected-lite new file mode 160000 index 00000000000..3634b0a6d8d --- /dev/null +++ b/lib/expected-lite @@ -0,0 +1 @@ +Subproject commit 3634b0a6d8dffcffad4d1355253d79290c0c754c diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 54906f56c75..233211bfa81 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -19,6 +19,11 @@ class EmptyApplication : public IApplication virtual ~EmptyApplication() = default; + bool isTest() const override + { + return true; + } + const Paths &getPaths() override { return this->paths_; @@ -137,7 +142,7 @@ class EmptyApplication : public IApplication return nullptr; } - Logging *getChatLogger() override + ILogging *getChatLogger() override { assert(!"getChatLogger was called without being initialized"); return nullptr; diff --git a/mocks/include/mocks/LinkResolver.hpp b/mocks/include/mocks/LinkResolver.hpp new file mode 100644 index 00000000000..8a5682a3d1c --- /dev/null +++ b/mocks/include/mocks/LinkResolver.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "providers/links/LinkResolver.hpp" + +#include +#include +#include + +namespace chatterino::mock { + +class LinkResolver : public ILinkResolver +{ +public: + LinkResolver() = default; + ~LinkResolver() override = default; + + MOCK_METHOD(void, resolve, (LinkInfo * info), (override)); +}; + +class EmptyLinkResolver : public ILinkResolver +{ +public: + EmptyLinkResolver() = default; + ~EmptyLinkResolver() override = default; + + void resolve(LinkInfo *info) override + { + // + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/Logging.hpp b/mocks/include/mocks/Logging.hpp new file mode 100644 index 00000000000..8d444142bb6 --- /dev/null +++ b/mocks/include/mocks/Logging.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "singletons/Logging.hpp" + +#include +#include +#include + +namespace chatterino::mock { + +class Logging : public ILogging +{ +public: + Logging() = default; + ~Logging() override = default; + + MOCK_METHOD(void, addMessage, + (const QString &channelName, MessagePtr message, + const QString &platformName), + (override)); +}; + +class EmptyLogging : public ILogging +{ +public: + EmptyLogging() = default; + ~EmptyLogging() override = default; + + void addMessage(const QString &channelName, MessagePtr message, + const QString &platformName) override + { + // + } +}; + +} // namespace chatterino::mock diff --git a/resources/licenses/expected-lite.txt b/resources/licenses/expected-lite.txt new file mode 100644 index 00000000000..36b7cd93cdf --- /dev/null +++ b/resources/licenses/expected-lite.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/src/Application.cpp b/src/Application.cpp index 77663923f88..5a9323921a7 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -504,7 +504,7 @@ PubSub *Application::getTwitchPubSub() return this->twitchPubSub.get(); } -Logging *Application::getChatLogger() +ILogging *Application::getChatLogger() { assertInGuiThread(); diff --git a/src/Application.hpp b/src/Application.hpp index 795a9f6d976..d2c0e2facc3 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -35,6 +35,7 @@ class PluginController; class Theme; class WindowManager; +class ILogging; class Logging; class Paths; class Emotes; @@ -64,6 +65,8 @@ class IApplication static IApplication *instance; + virtual bool isTest() const = 0; + virtual const Paths &getPaths() = 0; virtual const Args &getArgs() = 0; virtual Theme *getThemes() = 0; @@ -80,7 +83,7 @@ class IApplication virtual ITwitchIrcServer *getTwitch() = 0; virtual IAbstractIrcServer *getTwitchAbstract() = 0; virtual PubSub *getTwitchPubSub() = 0; - virtual Logging *getChatLogger() = 0; + virtual ILogging *getChatLogger() = 0; virtual IChatterinoBadges *getChatterinoBadges() = 0; virtual FfzBadges *getFfzBadges() = 0; virtual SeventvBadges *getSeventvBadges() = 0; @@ -121,6 +124,11 @@ class Application : public IApplication Application &operator=(const Application &) = delete; Application &operator=(Application &&) = delete; + bool isTest() const override + { + return false; + } + /** * In the interim, before we remove _exit(0); from RunGui.cpp, * this will destroy things we know can be destroyed @@ -191,7 +199,7 @@ class Application : public IApplication ITwitchIrcServer *getTwitch() override; IAbstractIrcServer *getTwitchAbstract() override; PubSub *getTwitchPubSub() override; - Logging *getChatLogger() override; + ILogging *getChatLogger() override; FfzBadges *getFfzBadges() override; SeventvBadges *getSeventvBadges() override; IUserDataController *getUserData() override; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 72fa4851ad7..da690e43a38 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -107,6 +107,8 @@ set(SOURCE_FILES controllers/commands/builtin/twitch/UpdateChannel.hpp controllers/commands/builtin/twitch/UpdateColor.cpp controllers/commands/builtin/twitch/UpdateColor.hpp + controllers/commands/common/ChannelAction.cpp + controllers/commands/common/ChannelAction.hpp controllers/commands/CommandContext.hpp controllers/commands/CommandController.cpp controllers/commands/CommandController.hpp @@ -1003,6 +1005,9 @@ target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} # semver dependency https://github.com/Neargye/semver target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/semver/include) +# expected-lite dependency https://github.com/martinmoene/expected-lite +target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/expected-lite/include) + # miniaudio dependency https://github.com/mackron/miniaudio if (USE_SYSTEM_MINIAUDIO) message(STATUS "Building with system miniaudio") diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index de4ef056c0c..a8cd8285d55 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -11,6 +11,7 @@ Q_LOGGING_CATEGORY(chatterinoArgs, "chatterino.args", logThreshold); Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold); Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold); Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold); +Q_LOGGING_CATEGORY(chatterinoCommands, "chatterino.commands", logThreshold); Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold); Q_LOGGING_CATEGORY(chatterinoCrashhandler, "chatterino.crashhandler", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index 36daa0e1e92..b814bb33246 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -7,6 +7,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoArgs); Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark); Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv); Q_DECLARE_LOGGING_CATEGORY(chatterinoCache); +Q_DECLARE_LOGGING_CATEGORY(chatterinoCommands); Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon); Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler); Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji); @@ -17,6 +18,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoHighlights); Q_DECLARE_LOGGING_CATEGORY(chatterinoHotkeys); Q_DECLARE_LOGGING_CATEGORY(chatterinoHTTP); Q_DECLARE_LOGGING_CATEGORY(chatterinoImage); +Q_DECLARE_LOGGING_CATEGORY(chatterinoImageuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoIrc); Q_DECLARE_LOGGING_CATEGORY(chatterinoIvr); Q_DECLARE_LOGGING_CATEGORY(chatterinoLiveupdates); @@ -26,7 +28,6 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); -Q_DECLARE_LOGGING_CATEGORY(chatterinoImageuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages); Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings); diff --git a/src/controllers/commands/builtin/twitch/Ban.cpp b/src/controllers/commands/builtin/twitch/Ban.cpp index 27b3d5a462d..bce3001f4b0 100644 --- a/src/controllers/commands/builtin/twitch/Ban.cpp +++ b/src/controllers/commands/builtin/twitch/Ban.cpp @@ -1,13 +1,14 @@ #include "controllers/commands/builtin/twitch/Ban.hpp" #include "Application.hpp" +#include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" -#include "util/Twitch.hpp" namespace { @@ -80,13 +81,12 @@ QString formatBanTimeoutError(const char *operation, HelixBanUserError error, return errorMessage; } -void banUserByID(const ChannelPtr &channel, const TwitchChannel *twitchChannel, +void banUserByID(const ChannelPtr &channel, const QString &channelID, const QString &sourceUserID, const QString &targetUserID, const QString &reason, const QString &displayName) { getHelix()->banUser( - twitchChannel->roomId(), sourceUserID, targetUserID, std::nullopt, - reason, + channelID, sourceUserID, targetUserID, std::nullopt, reason, [] { // No response for bans, they're emitted over pubsub/IRC instead }, @@ -97,14 +97,13 @@ void banUserByID(const ChannelPtr &channel, const TwitchChannel *twitchChannel, }); } -void timeoutUserByID(const ChannelPtr &channel, - const TwitchChannel *twitchChannel, +void timeoutUserByID(const ChannelPtr &channel, const QString &channelID, const QString &sourceUserID, const QString &targetUserID, int duration, const QString &reason, const QString &displayName) { getHelix()->banUser( - twitchChannel->roomId(), sourceUserID, targetUserID, duration, reason, + channelID, sourceUserID, targetUserID, duration, reason, [] { // No response for timeouts, they're emitted over pubsub/IRC instead }, @@ -121,63 +120,108 @@ namespace chatterino::commands { QString sendBan(const CommandContext &ctx) { - const auto &words = ctx.words; - const auto &channel = ctx.channel; - const auto *twitchChannel = ctx.twitchChannel; + const auto command = QStringLiteral("/ban"); + const auto usage = QStringLiteral( + R"(Usage: "/ban [options...] [reason]" - Permanently prevent a user from chatting via their username. Reason is optional and will be shown to the target user and other moderators. Options: --channel to override which channel the ban takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, false, true); - if (channel == nullptr) + if (!actions.has_value()) { - return ""; - } + if (ctx.channel != nullptr) + { + ctx.channel->addMessage(makeSystemMessage(actions.error())); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - QString("The /ban command only works in Twitch channels."))); return ""; } - const auto *usageStr = - "Usage: \"/ban [reason]\" - Permanently prevent a user " - "from chatting. Reason is optional and will be shown to the target " - "user and other moderators. Use \"/unban\" to remove a ban."; - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage(usageStr)); - return ""; - } + assert(!actions.value().empty()); auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); if (currentUser->isAnon()) { - channel->addMessage( + ctx.channel->addMessage( makeSystemMessage("You must be logged in to ban someone!")); return ""; } - const auto &rawTarget = words.at(1); - auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget); - auto reason = words.mid(2).join(' '); - - if (!targetUserID.isEmpty()) - { - banUserByID(channel, twitchChannel, currentUser->getUserId(), - targetUserID, reason, targetUserID); - } - else + for (const auto &action : actions.value()) { - getHelix()->getUserByName( - targetUserName, - [channel, currentUser, twitchChannel, - reason](const auto &targetUser) { - banUserByID(channel, twitchChannel, currentUser->getUserId(), - targetUser.id, reason, targetUser.displayName); - }, - [channel, targetUserName{targetUserName}] { - // Equivalent error from IRC - channel->addMessage(makeSystemMessage( - QString("Invalid username: %1").arg(targetUserName))); - }); + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Ban Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Ban Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to ban, bad channel name: %1") + .arg(actionChannel.login))); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to ban, bad target name: %1") + .arg(actionTarget.login))); + return; + } + + banUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addMessage(makeSystemMessage( + QString("Failed to ban, bad username(s): %1") + .arg(userLoginsToFetch.join(", ")))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + banUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, reason, + action.target.id); + } } return ""; @@ -221,87 +265,117 @@ QString sendBanById(const CommandContext &ctx) auto target = words.at(1); auto reason = words.mid(2).join(' '); - banUserByID(channel, twitchChannel, currentUser->getUserId(), target, - reason, target); + banUserByID(channel, twitchChannel->roomId(), currentUser->getUserId(), + target, reason, target); return ""; } QString sendTimeout(const CommandContext &ctx) { - const auto &words = ctx.words; - const auto &channel = ctx.channel; - const auto *twitchChannel = ctx.twitchChannel; + const auto command = QStringLiteral("/timeout"); + const auto usage = QStringLiteral( + R"(Usage: "/timeout [options...] [duration][time unit] [reason]" - Temporarily prevent a user from chatting. Duration (optional, default=10 minutes) must be a positive integer; time unit (optional, default=s) must be one of s, m, h, d, w; maximum duration is 2 weeks. Combinations like 1d2h are also allowed. Reason is optional and will be shown to the target user and other moderators. Use "/untimeout" to remove a timeout. Options: --channel to override which channel the timeout takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, true, true); - if (channel == nullptr) + if (!actions.has_value()) { - return ""; - } + if (ctx.channel != nullptr) + { + ctx.channel->addMessage(makeSystemMessage(actions.error())); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - QString("The /timeout command only works in Twitch channels."))); - return ""; - } - const auto *usageStr = - "Usage: \"/timeout [duration][time unit] [reason]\" - " - "Temporarily prevent a user from chatting. Duration (optional, " - "default=10 minutes) must be a positive integer; time unit " - "(optional, default=s) must be one of s, m, h, d, w; maximum " - "duration is 2 weeks. Combinations like 1d2h are also allowed. " - "Reason is optional and will be shown to the target user and other " - "moderators. Use \"/untimeout\" to remove a timeout."; - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage(usageStr)); return ""; } + assert(!actions.value().empty()); + auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); if (currentUser->isAnon()) { - channel->addMessage( + ctx.channel->addMessage( makeSystemMessage("You must be logged in to timeout someone!")); return ""; } - const auto &rawTarget = words.at(1); - auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget); - - int duration = 10 * 60; // 10min - if (words.size() >= 3) + for (const auto &action : actions.value()) { - duration = (int)parseDurationToSeconds(words.at(2)); - if (duration <= 0) + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) { - channel->addMessage(makeSystemMessage(usageStr)); - return ""; + assert(!action.target.login.isEmpty() && + "Timeout Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Timeout Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); } - } - auto reason = words.mid(3).join(' '); - if (!targetUserID.isEmpty()) - { - timeoutUserByID(channel, twitchChannel, currentUser->getUserId(), - targetUserID, duration, reason, targetUserID); - } - else - { - getHelix()->getUserByName( - targetUserName, - [channel, currentUser, twitchChannel, - targetUserName{targetUserName}, duration, - reason](const auto &targetUser) { - timeoutUserByID(channel, twitchChannel, - currentUser->getUserId(), targetUser.id, - duration, reason, targetUser.displayName); - }, - [channel, targetUserName{targetUserName}] { - // Equivalent error from IRC - channel->addMessage(makeSystemMessage( - QString("Invalid username: %1").arg(targetUserName))); - }); + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, duration{action.duration}, + actionChannel{action.channel}, actionTarget{action.target}, + currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad channel name: %1") + .arg(actionChannel.login))); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad target name: %1") + .arg(actionTarget.login))); + return; + } + + timeoutUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + duration, reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad username(s): %1") + .arg(userLoginsToFetch.join(", ")))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + timeoutUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, + action.duration, reason, action.target.id); + } } return ""; diff --git a/src/controllers/commands/builtin/twitch/Unban.cpp b/src/controllers/commands/builtin/twitch/Unban.cpp index e88008e8427..f47d701683f 100644 --- a/src/controllers/commands/builtin/twitch/Unban.cpp +++ b/src/controllers/commands/builtin/twitch/Unban.cpp @@ -1,24 +1,25 @@ +#include "controllers/commands/builtin/twitch/Unban.hpp" + #include "Application.hpp" +#include "common/Channel.hpp" +#include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" -#include "controllers/commands/builtin/twitch/Ban.hpp" #include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchAccount.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "util/Twitch.hpp" namespace { using namespace chatterino; -void unbanUserByID(const ChannelPtr &channel, - const TwitchChannel *twitchChannel, +void unbanUserByID(const ChannelPtr &channel, const QString &channelID, const QString &sourceUserID, const QString &targetUserID, const QString &displayName) { getHelix()->unbanUser( - twitchChannel->roomId(), sourceUserID, targetUserID, + channelID, sourceUserID, targetUserID, [] { // No response for unbans, they're emitted over pubsub/IRC instead }, @@ -85,27 +86,29 @@ namespace chatterino::commands { QString unbanUser(const CommandContext &ctx) { - if (ctx.channel == nullptr) + const auto command = ctx.words.at(0).toLower(); + const auto usage = + QStringLiteral( + R"(Usage: "%1 - Removes a ban on a user. Options: --channel to override which channel the unban takes place in (can be specified multiple times).)") + .arg(command); + const auto actions = parseChannelAction(ctx, command, usage, false, false); + if (!actions.has_value()) { - return ""; - } + if (ctx.channel != nullptr) + { + ctx.channel->addMessage(makeSystemMessage(actions.error())); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } - auto commandName = ctx.words.at(0).toLower(); - if (ctx.twitchChannel == nullptr) - { - ctx.channel->addMessage(makeSystemMessage( - QString("The %1 command only works in Twitch channels.") - .arg(commandName))); - return ""; - } - if (ctx.words.size() < 2) - { - ctx.channel->addMessage(makeSystemMessage( - QString("Usage: \"%1 \" - Removes a ban on a user.") - .arg(commandName))); return ""; } + assert(!actions.value().empty()); + auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); if (currentUser->isAnon()) { @@ -114,29 +117,78 @@ QString unbanUser(const CommandContext &ctx) return ""; } - const auto &rawTarget = ctx.words.at(1); - auto [targetUserName, targetUserID] = parseUserNameOrID(rawTarget); - - if (!targetUserID.isEmpty()) - { - unbanUserByID(ctx.channel, ctx.twitchChannel, currentUser->getUserId(), - targetUserID, targetUserID); - } - else + for (const auto &action : actions.value()) { - getHelix()->getUserByName( - targetUserName, - [channel{ctx.channel}, currentUser, - twitchChannel{ctx.twitchChannel}, - targetUserName{targetUserName}](const auto &targetUser) { - unbanUserByID(channel, twitchChannel, currentUser->getUserId(), - targetUser.id, targetUser.displayName); - }, - [channel{ctx.channel}, targetUserName{targetUserName}] { - // Equivalent error from IRC - channel->addMessage(makeSystemMessage( - QString("Invalid username: %1").arg(targetUserName))); - }); + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Unban Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Unban Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad channel name: %1") + .arg(actionChannel.login))); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad target name: %1") + .arg(actionTarget.login))); + return; + } + + unbanUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addMessage(makeSystemMessage( + QString("Failed to timeout, bad username(s): %1") + .arg(userLoginsToFetch.join(", ")))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + unbanUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, + action.target.id); + } } return ""; diff --git a/src/controllers/commands/common/ChannelAction.cpp b/src/controllers/commands/common/ChannelAction.cpp new file mode 100644 index 00000000000..4873859207d --- /dev/null +++ b/src/controllers/commands/common/ChannelAction.cpp @@ -0,0 +1,184 @@ +#include "controllers/commands/common/ChannelAction.hpp" + +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Helpers.hpp" +#include "util/Twitch.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace chatterino::commands { + +bool IncompleteHelixUser::hydrateFrom(const std::vector &users) +{ + // Find user in list based on our id or login + auto resolvedIt = + std::find_if(users.begin(), users.end(), [this](const auto &user) { + if (!this->login.isEmpty()) + { + return user.login.compare(this->login, Qt::CaseInsensitive) == + 0; + } + if (!this->id.isEmpty()) + { + return user.id.compare(this->id, Qt::CaseInsensitive) == 0; + } + return false; + }); + if (resolvedIt == users.end()) + { + return false; + } + const auto &resolved = *resolvedIt; + this->id = resolved.id; + this->login = resolved.login; + this->displayName = resolved.displayName; + return true; +} + +std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u) +{ + os << "{id:" << u.id.toStdString() << ", login:" << u.login.toStdString() + << ", displayName:" << u.displayName.toStdString() << '}'; + return os; +} + +void PrintTo(const PerformChannelAction &a, std::ostream *os) +{ + *os << "{channel:" << a.channel << ", target:" << a.target + << ", reason:" << a.reason.toStdString() + << ", duration:" << std::to_string(a.duration) << '}'; +} + +nonstd::expected, QString> parseChannelAction( + const CommandContext &ctx, const QString &command, const QString &usage, + bool withDuration, bool withReason) +{ + if (ctx.channel == nullptr) + { + // A ban action must be performed with a channel as a context + return nonstd::make_unexpected( + "A " % command % + " action must be performed with a channel as a context"); + } + + QCommandLineParser parser; + parser.setOptionsAfterPositionalArgumentsMode( + QCommandLineParser::ParseAsPositionalArguments); + parser.addPositionalArgument("username", "The name of the user to ban"); + if (withDuration) + { + parser.addPositionalArgument("duration", "Duration of the action"); + } + if (withReason) + { + parser.addPositionalArgument("reason", "The optional ban reason"); + } + QCommandLineOption channelOption( + "channel", "Override which channel(s) to perform the action in", + "channel"); + parser.addOptions({ + channelOption, + }); + parser.parse(ctx.words); + + auto positionalArguments = parser.positionalArguments(); + if (positionalArguments.isEmpty()) + { + return nonstd::make_unexpected("Missing target - " % usage); + } + + auto [targetUserName, targetUserID] = + parseUserNameOrID(positionalArguments.takeFirst()); + + PerformChannelAction base{ + .target = + IncompleteHelixUser{ + .id = targetUserID, + .login = targetUserName, + .displayName = "", + }, + .duration = 0, + }; + + if (withDuration) + { + if (positionalArguments.isEmpty()) + { + base.duration = 10 * 60; // 10 min + } + else + { + auto durationStr = positionalArguments.takeFirst(); + base.duration = (int)parseDurationToSeconds(durationStr); + if (base.duration <= 0) + { + return nonstd::make_unexpected("Invalid duration - " % usage); + } + if (withReason) + { + base.reason = positionalArguments.join(' '); + } + } + } + else + { + if (withReason) + { + base.reason = positionalArguments.join(' '); + } + } + + std::vector actions; + + auto overrideChannels = parser.values(channelOption); + if (overrideChannels.isEmpty()) + { + if (ctx.twitchChannel == nullptr) + { + return nonstd::make_unexpected( + "The " % command % " command only works in Twitch channels"); + } + + actions.push_back(PerformChannelAction{ + .channel = + { + .id = ctx.twitchChannel->roomId(), + .login = ctx.twitchChannel->getName(), + .displayName = ctx.twitchChannel->getDisplayName(), + }, + .target = base.target, + .reason = base.reason, + .duration = base.duration, + }); + } + else + { + for (const auto &overrideChannelTarget : overrideChannels) + { + auto [channelUserName, channelUserID] = + parseUserNameOrID(overrideChannelTarget); + actions.push_back(PerformChannelAction{ + .channel = + { + .id = channelUserID, + .login = channelUserName, + }, + .target = base.target, + .reason = base.reason, + .duration = base.duration, + }); + } + } + + return actions; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/common/ChannelAction.hpp b/src/controllers/commands/common/ChannelAction.hpp new file mode 100644 index 00000000000..fd80eeb7954 --- /dev/null +++ b/src/controllers/commands/common/ChannelAction.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace chatterino { + +struct CommandContext; +struct HelixUser; + +} // namespace chatterino + +namespace chatterino::commands { + +struct IncompleteHelixUser { + QString id; + QString login; + QString displayName; + + bool hydrateFrom(const std::vector &users); + + bool operator==(const IncompleteHelixUser &other) const + { + return std::tie(this->id, this->login, this->displayName) == + std::tie(other.id, other.login, other.displayName); + } +}; + +struct PerformChannelAction { + // Channel to perform the action in + IncompleteHelixUser channel; + // Target to perform the action on + IncompleteHelixUser target; + QString reason; + int duration{}; + + bool operator==(const PerformChannelAction &other) const + { + return std::tie(this->channel, this->target, this->reason, + this->duration) == std::tie(other.channel, other.target, + other.reason, + other.duration); + } +}; + +std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u); +// gtest printer +// NOLINTNEXTLINE(readability-identifier-naming) +void PrintTo(const PerformChannelAction &a, std::ostream *os); + +nonstd::expected, QString> parseChannelAction( + const CommandContext &ctx, const QString &command, const QString &usage, + bool withDuration, bool withReason); + +} // namespace chatterino::commands diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 4339d6c2ec6..bb982c8fe3e 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -91,13 +91,6 @@ TwitchChannel::TwitchChannel(const QString &name) { qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened"; - if (!getApp()) - { - // This is intended for tests and benchmarks. - // Irc, Pubsub, live-updates, and live-notifications aren't mocked there. - return; - } - this->bSignals_.emplace_back( getIApp()->getAccounts()->twitch.currentUserChanged.connect([this] { this->setMod(false); @@ -231,13 +224,6 @@ TwitchChannel::TwitchChannel(const QString &name) TwitchChannel::~TwitchChannel() { - if (!getApp()) - { - // This is for tests and benchmarks, where live-updates aren't mocked - // see comment in constructor. - return; - } - getIApp()->getTwitch()->dropSeventvChannel(this->seventvUserID_, this->seventvEmoteSetID_); @@ -586,6 +572,10 @@ void TwitchChannel::showLoginMessage() void TwitchChannel::roomIdChanged() { + if (getIApp()->isTest()) + { + return; + } this->refreshPubSub(); this->refreshBadges(); this->refreshCheerEmotes(); @@ -792,7 +782,7 @@ void TwitchChannel::setRoomId(const QString &id) { *this->roomID_.access() = id; // This is intended for tests and benchmarks. See comment in constructor. - if (getApp()) + if (!getIApp()->isTest()) { this->roomIdChanged(); this->loadRecentMessages(); @@ -1341,6 +1331,11 @@ void TwitchChannel::loadRecentMessagesReconnect() void TwitchChannel::refreshPubSub() { + if (getIApp()->isTest()) + { + return; + } + auto roomId = this->roomId(); if (roomId.isEmpty()) { diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 2add5430213..611fea1f9a0 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -463,6 +463,7 @@ class TwitchChannel final : public Channel, public ChannelChatters friend class TwitchIrcServer; friend class TwitchMessageBuilder; friend class IrcMessageHandler; + friend class Commands_E2E_Test; }; } // namespace chatterino diff --git a/src/singletons/Logging.hpp b/src/singletons/Logging.hpp index edd1ac07fc9..af86a702dcf 100644 --- a/src/singletons/Logging.hpp +++ b/src/singletons/Logging.hpp @@ -16,13 +16,22 @@ struct Message; using MessagePtr = std::shared_ptr; class LoggingChannel; -class Logging +class ILogging +{ +public: + virtual ~ILogging() = default; + + virtual void addMessage(const QString &channelName, MessagePtr message, + const QString &platformName) = 0; +}; + +class Logging : public ILogging { public: Logging(Settings &settings); void addMessage(const QString &channelName, MessagePtr message, - const QString &platformName); + const QString &platformName) override; private: using PlatformName = QString; diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 89c985c5e8d..bc503ca6107 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -127,6 +127,9 @@ AboutPage::AboutPage() addLicense(form.getElement(), "Fluent icons", "https://github.com/microsoft/fluentui-system-icons", ":/licenses/fluenticons.txt"); + addLicense(form.getElement(), "expected-lite", + "https://github.com/martinmoene/expected-lite", + ":/licenses/expected-lite.txt"); } // Attributions diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8288664dfd8..19ac9195b31 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/QMagicEnum.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ModerationAction.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp # Add your new file above this line! ) diff --git a/tests/src/Commands.cpp b/tests/src/Commands.cpp new file mode 100644 index 00000000000..f180a718080 --- /dev/null +++ b/tests/src/Commands.cpp @@ -0,0 +1,1057 @@ +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/Command.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/CommandController.hpp" +#include "controllers/commands/common/ChannelAction.hpp" +#include "mocks/EmptyApplication.hpp" +#include "mocks/Helix.hpp" +#include "mocks/Logging.hpp" +#include "mocks/TwitchIrcServer.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Settings.hpp" +#include "Test.hpp" + +#include + +using namespace chatterino; + +using ::testing::_; +using ::testing::StrictMock; + +namespace { + +class MockApplication : mock::EmptyApplication +{ +public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + { + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + AccountController *getAccounts() override + { + return &this->accounts; + } + + CommandController *getCommands() override + { + return &this->commands; + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + ILogging *getChatLogger() override + { + return &this->chatLogger; + } + + Settings settings; + AccountController accounts; + CommandController commands; + mock::MockTwitchIrcServer twitch; + Emotes emotes; + mock::EmptyLogging chatLogger; +}; + +} // namespace + +namespace chatterino { + +TEST(Commands, parseBanActions) +{ + MockApplication app; + + std::shared_ptr channel = + std::make_shared("forsen"); + + CommandContext ctx{}; + + QString command("/ban"); + QString usage("usage string"); + bool withDuration = false; + bool withReason = true; + + struct Test { + CommandContext inputContext; + + std::vector expectedActions; + QString expectedError; + }; + + std::vector tests{ + { + // Normal ban with an added reason, with the user maybe trying to use the --channel parameter at the end, but it gets eaten by the reason + .inputContext = + { + .words = {"/ban", "forsen", "the", "ban", "reason", + "--channel", "xD"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "the ban reason --channel xD", + .duration = 0, + }, + }, + .expectedError = "", + }, + { + // Normal ban with an added reason + .inputContext = + { + .words = {"/ban", "forsen", "the", "ban", "reason"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "the ban reason", + .duration = 0, + }, + }, + .expectedError = "", + }, + { + // Normal ban without an added reason + .inputContext = + { + .words = {"/ban", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 0, + }, + }, + .expectedError = "", + }, + { + // User forgot to specify who to ban + .inputContext = + { + .words = {"/ban"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User tried to use /ban outside of a channel context (shouldn't really be able to happen) + .inputContext = + { + .words = {"/ban"}, + }, + .expectedActions = {}, + .expectedError = + "A " % command % + " action must be performed with a channel as a context", + }, + { + // User tried to use /ban without a target, but with a --channel specified + .inputContext = + { + .words = {"/ban", "--channel", "pajlada"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User overriding the ban to be done in the pajlada channel + .inputContext = + { + .words = {"/ban", "--channel", "pajlada", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 0, + }, + }, + .expectedError = "", + }, + { + // User overriding the ban to be done in the pajlada channel and in the channel with the id 11148817 + .inputContext = + { + .words = {"/ban", "--channel", "pajlada", "--channel", + "id:11148817", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 0, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 0, + }, + }, + .expectedError = "", + }, + { + // User overriding the ban to be done in the pajlada channel and in the channel with the id 11148817, with a reason specified + .inputContext = + { + .words = {"/ban", "--channel", "pajlada", "--channel", + "id:11148817", "forsen", "the", "ban", "reason"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "the ban reason", + .duration = 0, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + .reason = "the ban reason", + .duration = 0, + }, + }, + .expectedError = "", + }, + }; + + for (const auto &test : tests) + { + auto oActions = commands::parseChannelAction( + test.inputContext, command, usage, withDuration, withReason); + + if (!test.expectedActions.empty()) + { + ASSERT_TRUE(oActions.has_value()) << oActions.error(); + auto actions = *oActions; + ASSERT_EQ(actions.size(), test.expectedActions.size()); + ASSERT_EQ(actions, test.expectedActions); + } + else + { + ASSERT_FALSE(oActions.has_value()); + } + + if (!test.expectedError.isEmpty()) + { + ASSERT_FALSE(oActions.has_value()); + ASSERT_EQ(oActions.error(), test.expectedError); + } + } +} + +TEST(Commands, parseTimeoutActions) +{ + MockApplication app; + + std::shared_ptr channel = + std::make_shared("forsen"); + + CommandContext ctx{}; + + QString command("/timeout"); + QString usage("usage string"); + bool withDuration = true; + bool withReason = true; + + struct Test { + CommandContext inputContext; + + std::vector expectedActions; + QString expectedError; + }; + + std::vector tests{ + { + // Normal timeout without an added reason, with the default duration + .inputContext = + { + .words = {"/timeout", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 10 * 60, + }, + }, + .expectedError = "", + }, + { + // Normal timeout without an added reason, with a custom duration + .inputContext = + { + .words = {"/timeout", "forsen", "5m"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 5 * 60, + }, + }, + .expectedError = "", + }, + { + // Normal timeout without an added reason, with a custom duration, with an added reason + .inputContext = + { + .words = {"/timeout", "forsen", "5m", "the", "timeout", + "reason"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + .reason = "the timeout reason", + .duration = 5 * 60, + }, + }, + .expectedError = "", + }, + { + // Normal timeout without an added reason, with an added reason, but user forgot to specify a timeout duration so it fails + .inputContext = + { + .words = {"/timeout", "forsen", "the", "timeout", "reason"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Invalid duration - " % usage, + }, + { + // User forgot to specify who to timeout + .inputContext = + { + .words = {"/timeout"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User tried to use /timeout outside of a channel context (shouldn't really be able to happen) + .inputContext = + { + .words = {"/timeout"}, + }, + .expectedActions = {}, + .expectedError = + "A " % command % + " action must be performed with a channel as a context", + }, + { + // User tried to use /timeout without a target, but with a --channel specified + .inputContext = + { + .words = {"/timeout", "--channel", "pajlada"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User overriding the timeout to be done in the pajlada channel + .inputContext = + { + .words = {"/timeout", "--channel", "pajlada", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 10 * 60, + }, + }, + .expectedError = "", + }, + { + // User overriding the timeout to be done in the pajlada channel and in the channel with the id 11148817 + .inputContext = + { + .words = {"/timeout", "--channel", "pajlada", "--channel", + "id:11148817", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 10 * 60, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 10 * 60, + }, + }, + .expectedError = "", + }, + { + // User overriding the timeout to be done in the pajlada channel and in the channel with the id 11148817, with a custom duration + .inputContext = + { + .words = {"/timeout", "--channel", "pajlada", "--channel", + "id:11148817", "forsen", "5m"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 5 * 60, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + .reason = "", + .duration = 5 * 60, + }, + }, + .expectedError = "", + }, + { + // User overriding the timeout to be done in the pajlada channel and in the channel with the id 11148817, with a reason specified + .inputContext = + { + .words = {"/timeout", "--channel", "pajlada", "--channel", + "id:11148817", "forsen", "10m", "the", "timeout", + "reason"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + .reason = "the timeout reason", + .duration = 10 * 60, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + .reason = "the timeout reason", + .duration = 10 * 60, + }, + }, + .expectedError = "", + }, + }; + + for (const auto &test : tests) + { + auto oActions = commands::parseChannelAction( + test.inputContext, command, usage, withDuration, withReason); + + if (!test.expectedActions.empty()) + { + ASSERT_TRUE(oActions.has_value()) << oActions.error(); + auto actions = *oActions; + ASSERT_EQ(actions.size(), test.expectedActions.size()); + ASSERT_EQ(actions, test.expectedActions); + } + else + { + ASSERT_FALSE(oActions.has_value()); + } + + if (!test.expectedError.isEmpty()) + { + ASSERT_FALSE(oActions.has_value()); + ASSERT_EQ(oActions.error(), test.expectedError); + } + } +} + +TEST(Commands, parseUnbanActions) +{ + MockApplication app; + + std::shared_ptr channel = + std::make_shared("forsen"); + + CommandContext ctx{}; + + QString command("/unban"); + QString usage("usage string"); + bool withDuration = false; + bool withReason = false; + + struct Test { + CommandContext inputContext; + + std::vector expectedActions; + QString expectedError; + }; + + std::vector tests{ + { + // Normal unban + .inputContext = + { + .words = {"/unban", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + }, + }, + .expectedError = "", + }, + { + // Normal unban but user input some random junk after the target + .inputContext = + { + .words = {"/unban", "forsen", "foo", "bar", "baz"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "forsen", + .displayName = "forsen", + }, + .target = + { + .login = "forsen", + }, + }, + }, + .expectedError = "", + }, + { + // User forgot to specify who to unban + .inputContext = + { + .words = {"/unban"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User tried to use /unban outside of a channel context (shouldn't really be able to happen) + .inputContext = + { + .words = {"/unban"}, + }, + .expectedActions = {}, + .expectedError = + "A " % command % + " action must be performed with a channel as a context", + }, + { + // User tried to use /unban without a target, but with a --channel specified + .inputContext = + { + .words = {"/unban", "--channel", "pajlada"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = {}, + .expectedError = "Missing target - " % usage, + }, + { + // User overriding the unban to be done in the pajlada channel + .inputContext = + { + .words = {"/unban", "--channel", "pajlada", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + }, + }, + .expectedError = "", + }, + { + // User overriding the unban to be done in the pajlada channel and in the channel with the id 11148817 + .inputContext = + { + .words = {"/unban", "--channel", "pajlada", "--channel", + "id:11148817", "forsen"}, + .channel = channel, + .twitchChannel = channel.get(), + }, + .expectedActions = + { + { + .channel = + { + .login = "pajlada", + }, + .target = + { + .login = "forsen", + }, + }, + { + .channel = + { + .id = "11148817", + }, + .target = + { + .login = "forsen", + }, + }, + }, + .expectedError = "", + }, + }; + + for (const auto &test : tests) + { + auto oActions = commands::parseChannelAction( + test.inputContext, command, usage, withDuration, withReason); + + if (!test.expectedActions.empty()) + { + ASSERT_TRUE(oActions.has_value()) << oActions.error(); + auto actions = *oActions; + ASSERT_EQ(actions.size(), test.expectedActions.size()); + ASSERT_EQ(actions, test.expectedActions); + } + else + { + ASSERT_FALSE(oActions.has_value()); + } + + if (!test.expectedError.isEmpty()) + { + ASSERT_FALSE(oActions.has_value()); + ASSERT_EQ(oActions.error(), test.expectedError); + } + } +} + +TEST(Commands, E2E) +{ + ::testing::InSequence seq; + MockApplication app; + + app.commands.initialize(*getSettings(), getIApp()->getPaths()); + + QJsonObject pajlada; + pajlada["id"] = "11148817"; + pajlada["login"] = "pajlada"; + pajlada["display_name"] = "pajlada"; + pajlada["created_at"] = "2010-03-17T11:50:53Z"; + pajlada["description"] = " ͡° ͜ʖ ͡°)"; + pajlada["profile_image_url"] = + "https://static-cdn.jtvnw.net/jtv_user_pictures/" + "cbe986e3-06ad-4506-a3aa-eb05466c839c-profile_image-300x300.png"; + + QJsonObject testaccount420; + testaccount420["id"] = "117166826"; + testaccount420["login"] = "testaccount_420"; + testaccount420["display_name"] = "테스트계정420"; + testaccount420["created_at"] = "2016-02-27T18:55:59Z"; + testaccount420["description"] = ""; + testaccount420["profile_image_url"] = + "https://static-cdn.jtvnw.net/user-default-pictures-uv/" + "ead5c8b2-a4c9-4724-b1dd-9f00b46cbd3d-profile_image-300x300.png"; + + QJsonObject forsen; + forsen["id"] = "22484632"; + forsen["login"] = "forsen"; + forsen["display_name"] = "Forsen"; + forsen["created_at"] = "2011-05-19T00:28:28Z"; + forsen["description"] = + "Approach with caution! No roleplaying or tryharding allowed."; + forsen["profile_image_url"] = + "https://static-cdn.jtvnw.net/jtv_user_pictures/" + "forsen-profile_image-48b43e1e4f54b5c8-300x300.png"; + + std::shared_ptr channel = + std::make_shared("pajlada"); + channel->setRoomId("11148817"); + + StrictMock mockHelix; + initializeHelix(&mockHelix); + + EXPECT_CALL(mockHelix, update).Times(1); + EXPECT_CALL(mockHelix, loadBlocks).Times(1); + + auto account = std::make_shared( + testaccount420["login"].toString(), "token", "oauthclient", + testaccount420["id"].toString()); + getIApp()->getAccounts()->twitch.accounts.append(account); + getIApp()->getAccounts()->twitch.currentUsername = + testaccount420["login"].toString(); + getIApp()->getAccounts()->twitch.load(); + + // Simple single-channel ban + EXPECT_CALL(mockHelix, fetchUsers(QStringList{"11148817"}, + QStringList{"forsen"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(pajlada), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, + banUser(pajlada["id"].toString(), QString("117166826"), + forsen["id"].toString(), std::optional{}, + QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand("/ban forsen", channel, false); + + // Multi-channel ban + EXPECT_CALL(mockHelix, fetchUsers(QStringList{"11148817"}, + QStringList{"forsen"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(pajlada), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, banUser(pajlada["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), + std::optional{}, QString(""), _, _)) + .Times(1); + + EXPECT_CALL(mockHelix, + fetchUsers(QStringList{}, + QStringList{"forsen", "testaccount_420"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(testaccount420), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, banUser(testaccount420["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), + std::optional{}, QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand( + "/ban --channel id:11148817 --channel testaccount_420 forsen", channel, + false); + + // ID-based ban + EXPECT_CALL(mockHelix, + banUser(pajlada["id"].toString(), QString("117166826"), + forsen["id"].toString(), std::optional{}, + QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand("/ban id:22484632", channel, false); + + // ID-based redirected ban + EXPECT_CALL(mockHelix, + banUser(testaccount420["id"].toString(), QString("117166826"), + forsen["id"].toString(), std::optional{}, + QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand( + "/ban --channel id:117166826 id:22484632", channel, false); + + // name-based redirected ban + EXPECT_CALL(mockHelix, fetchUsers(QStringList{"22484632"}, + QStringList{"testaccount_420"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(testaccount420), + HelixUser(forsen), + }; + success(users); + }); + EXPECT_CALL(mockHelix, + banUser(testaccount420["id"].toString(), QString("117166826"), + forsen["id"].toString(), std::optional{}, + QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand( + "/ban --channel testaccount_420 id:22484632", channel, false); + + // Multi-channel timeout + EXPECT_CALL(mockHelix, fetchUsers(QStringList{"11148817"}, + QStringList{"forsen"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(pajlada), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, banUser(pajlada["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), + std::optional{600}, QString(""), _, _)) + .Times(1); + + EXPECT_CALL(mockHelix, + fetchUsers(QStringList{}, + QStringList{"forsen", "testaccount_420"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(testaccount420), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, banUser(testaccount420["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), + std::optional{600}, QString(""), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand( + "/timeout --channel id:11148817 --channel testaccount_420 forsen", + channel, false); + + // Multi-channel unban + EXPECT_CALL(mockHelix, fetchUsers(QStringList{"11148817"}, + QStringList{"forsen"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(pajlada), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, unbanUser(pajlada["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), _, _)) + .Times(1); + + EXPECT_CALL(mockHelix, + fetchUsers(QStringList{}, + QStringList{"forsen", "testaccount_420"}, _, _)) + .WillOnce([=](auto, auto, auto success, auto) { + std::vector users{ + HelixUser(testaccount420), + HelixUser(forsen), + }; + success(users); + }); + + EXPECT_CALL(mockHelix, unbanUser(testaccount420["id"].toString(), + testaccount420["id"].toString(), + forsen["id"].toString(), _, _)) + .Times(1); + + getIApp()->getCommands()->execCommand( + "/unban --channel id:11148817 --channel testaccount_420 forsen", + channel, false); +} + +} // namespace chatterino From 2b97c64f8e2c3bb37066b8ebebc555e8bde3eac6 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 16 Jun 2024 13:26:40 +0200 Subject: [PATCH 086/105] Show line indicator instead of rectangle while dragging in tables (#5256) --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/widgets/helper/EditableModelView.cpp | 3 + src/widgets/helper/TableStyles.cpp | 71 ++++++++++++++++++++++++ src/widgets/helper/TableStyles.hpp | 32 +++++++++++ 5 files changed, 109 insertions(+) create mode 100644 src/widgets/helper/TableStyles.cpp create mode 100644 src/widgets/helper/TableStyles.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index e49cea9ba65..078d0ee400c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Minor: Improved error messages for channel update commands. (#5429) - Minor: Moderators can now see when users are warned. (#5441) - Minor: Added support for Brave & google-chrome-stable browsers. (#5452) +- Minor: Added drop indicator line while dragging in tables. (#5256) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index da690e43a38..82634375545 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -655,6 +655,8 @@ set(SOURCE_FILES widgets/helper/SettingsDialogTab.hpp widgets/helper/SignalLabel.cpp widgets/helper/SignalLabel.hpp + widgets/helper/TableStyles.cpp + widgets/helper/TableStyles.hpp widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButtons.cpp diff --git a/src/widgets/helper/EditableModelView.cpp b/src/widgets/helper/EditableModelView.cpp index b549c9e9ba5..cbc04c03b32 100644 --- a/src/widgets/helper/EditableModelView.cpp +++ b/src/widgets/helper/EditableModelView.cpp @@ -1,6 +1,7 @@ #include "EditableModelView.hpp" #include "widgets/helper/RegExpItemDelegate.hpp" +#include "widgets/helper/TableStyles.hpp" #include #include @@ -28,6 +29,8 @@ EditableModelView::EditableModelView(QAbstractTableModel *model, bool movable) this->tableView_->verticalHeader()->setVisible(false); this->tableView_->horizontalHeader()->setSectionsClickable(false); + TableRowDragStyle::applyTo(this->tableView_); + // create layout QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(0, 0, 0, 0); diff --git a/src/widgets/helper/TableStyles.cpp b/src/widgets/helper/TableStyles.cpp new file mode 100644 index 00000000000..bf586f198a6 --- /dev/null +++ b/src/widgets/helper/TableStyles.cpp @@ -0,0 +1,71 @@ +#include "widgets/helper/TableStyles.hpp" + +#include +#include +#include +#include +#include + +namespace chatterino { + +TableRowDragStyle::TableRowDragStyle(const QString &name) + : QProxyStyle(name) +{ +} + +void TableRowDragStyle::applyTo(QTableView *view) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + auto styleName = view->style()->name(); +#else + QString styleName = "fusion"; +#endif + auto *proxyStyle = new TableRowDragStyle(styleName); + proxyStyle->setParent(view); + view->setStyle(proxyStyle); +} + +void TableRowDragStyle::drawPrimitive(QStyle::PrimitiveElement element, + const QStyleOption *option, + QPainter *painter, + const QWidget *widget) const +{ + if (element != QStyle::PE_IndicatorItemViewItemDrop) + { + QProxyStyle::drawPrimitive(element, option, painter, widget); + return; + } + + const auto *view = dynamic_cast(widget); + if (!view) + { + assert(false && "TableStyle must be used on a QAbstractItemView"); + return; + } + + if (option->rect.isNull()) + { + return; + } + + // Get the direction a row is dragged in + auto selected = view->currentIndex(); + auto hovered = view->indexAt(option->rect.center()); + if (!selected.isValid() || !hovered.isValid()) + { + // This shouldn't happen as we're in a drag operation + assert(false && "Got bad indices"); + return; + } + + int y = option->rect.top(); // move up + if (hovered.row() >= selected.row()) + { + y = option->rect.bottom(); // move down + } + + painter->setPen({Qt::white, 2}); + painter->drawLine(0, y, widget->width(), y); +} + +} // namespace chatterino diff --git a/src/widgets/helper/TableStyles.hpp b/src/widgets/helper/TableStyles.hpp new file mode 100644 index 00000000000..a8d264831d0 --- /dev/null +++ b/src/widgets/helper/TableStyles.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +class QTableView; + +namespace chatterino { + +/// @brief A custom style for drag operations of rows on tables +/// +/// This style overwrites how `PE_IndicatorItemViewItemDrop`, the drop +/// indicator of item-views, is drawn. It's intended to be used on QTableViews +/// where entire rows are moved (not individual cells). The indicator is shown +/// as a line at the position where the dragged item should be inserted. If no +/// such position exists, a red border is drawn around the viewport. +class TableRowDragStyle : public QProxyStyle +{ +public: + /// Applies the style to @a view + static void applyTo(QTableView *view); + + void drawPrimitive(QStyle::PrimitiveElement element, + const QStyleOption *option, QPainter *painter, + const QWidget *widget = nullptr) const override; + +private: + /// @param name The style name to emulate. + /// This should be set to `style()->name()`. + TableRowDragStyle(const QString &name); +}; + +} // namespace chatterino From a714bf066fc788395b578fb2412161e1deef63a2 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 13:30:17 +0200 Subject: [PATCH 087/105] fix: git permission errors in Ubuntu builds (#5458) --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a28072ddddd..cc402fd7448 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,10 @@ jobs: submodules: recursive fetch-depth: 0 # allows for tags access + - name: Fix git permission error + run: | + git config --global --add safe.directory '*' + - name: Build run: | mkdir build From c9a0691f532566cd5da279aba293341ca3c8dec5 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 13:59:12 +0200 Subject: [PATCH 088/105] chore: unsingletonize UserDataController (#5459) The `user-data.json` file will save immediately on change, and on exit (on dtor) if necessary. So we don't need to manually call save --- CHANGELOG.md | 1 + src/Application.cpp | 5 +++-- src/Application.hpp | 2 +- src/controllers/userdata/UserDataController.cpp | 5 ----- src/controllers/userdata/UserDataController.hpp | 6 +----- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078d0ee400c..3b43a309698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Dev: Images are now loaded in worker threads. (#5431) - Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) - Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) +- Dev: Refactor/unsingletonize `UserDataController`. (#5459) ## 2.5.1 diff --git a/src/Application.cpp b/src/Application.cpp index 5a9323921a7..a3017302e8f 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -135,7 +135,7 @@ Application::Application(Settings &_settings, const Paths &paths, , twitch(new TwitchIrcServer) , ffzBadges(&this->emplace()) , seventvBadges(&this->emplace()) - , userData(&this->emplace(new UserDataController(paths))) + , userData(new UserDataController(paths)) , sound(&this->emplace(makeSoundController(_settings))) , twitchLiveController(&this->emplace()) , twitchPubSub(new PubSub(TWITCH_PUBSUB_URL)) @@ -173,6 +173,7 @@ void Application::fakeDtor() this->seventvEmotes.reset(); // this->twitch.reset(); this->fonts.reset(); + this->userData.reset(); } void Application::initialize(Settings &settings, const Paths &paths) @@ -427,7 +428,7 @@ IUserDataController *Application::getUserData() { assertInGuiThread(); - return this->userData; + return this->userData.get(); } ISoundController *Application::getSound() diff --git a/src/Application.hpp b/src/Application.hpp index d2c0e2facc3..4cce1aa8ec6 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -160,7 +160,7 @@ class Application : public IApplication std::unique_ptr twitch; FfzBadges *const ffzBadges{}; SeventvBadges *const seventvBadges{}; - UserDataController *const userData{}; + std::unique_ptr userData; ISoundController *const sound{}; TwitchLiveController *const twitchLiveController{}; std::unique_ptr twitchPubSub; diff --git a/src/controllers/userdata/UserDataController.cpp b/src/controllers/userdata/UserDataController.cpp index 3f8029cbefa..dc79f16ae22 100644 --- a/src/controllers/userdata/UserDataController.cpp +++ b/src/controllers/userdata/UserDataController.cpp @@ -37,11 +37,6 @@ UserDataController::UserDataController(const Paths &paths) this->users = this->setting.getValue(); } -void UserDataController::save() -{ - this->sm->save(); -} - std::optional UserDataController::getUser(const QString &userID) const { std::shared_lock lock(this->usersMutex); diff --git a/src/controllers/userdata/UserDataController.hpp b/src/controllers/userdata/UserDataController.hpp index 3be1f260adc..49edbde75f6 100644 --- a/src/controllers/userdata/UserDataController.hpp +++ b/src/controllers/userdata/UserDataController.hpp @@ -1,6 +1,5 @@ #pragma once -#include "common/Singleton.hpp" #include "controllers/userdata/UserData.hpp" #include "util/QStringHash.hpp" #include "util/RapidjsonHelpers.hpp" @@ -30,7 +29,7 @@ class IUserDataController const QString &colorString) = 0; }; -class UserDataController : public IUserDataController, public Singleton +class UserDataController : public IUserDataController { public: explicit UserDataController(const Paths &paths); @@ -43,9 +42,6 @@ class UserDataController : public IUserDataController, public Singleton void setUserColor(const QString &userID, const QString &colorString) override; -protected: - void save() override; - private: void update(std::unordered_map &&newUsers); From d053a681a0096dd4d0d8af5d879ca6f20a519311 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 14:39:30 +0200 Subject: [PATCH 089/105] make TwitchPubSubClient.DisconnectedAfter1s test less flaky (#5461) --- tests/src/TwitchPubSubClient.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index 728b0e5bbcc..dff88450f04 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -179,14 +179,7 @@ TEST(TwitchPubSubClient, DisconnectedAfter1s) ASSERT_EQ(pubSub.diag.messagesReceived, 2); // Listen RESPONSE & Pong ASSERT_EQ(pubSub.diag.listenResponses, 1); - std::this_thread::sleep_for(350ms); - - ASSERT_EQ(pubSub.diag.connectionsOpened, 1); - ASSERT_EQ(pubSub.diag.connectionsClosed, 0); - ASSERT_EQ(pubSub.diag.connectionsFailed, 0); - ASSERT_EQ(pubSub.diag.messagesReceived, 2); - - std::this_thread::sleep_for(600ms); + std::this_thread::sleep_for(950ms); ASSERT_EQ(pubSub.diag.connectionsOpened, 2); ASSERT_EQ(pubSub.diag.connectionsClosed, 1); From 85d6ff1e6c711c09c6904434a3d18c3d763f2305 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 15:17:07 +0200 Subject: [PATCH 090/105] chore: unsingletonize Resources/Resources2 (#5460) --- CHANGELOG.md | 1 + cmake/resources/ResourcesAutogen.hpp.in | 5 ++--- src/singletons/Resources.cpp | 11 ++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b43a309698..413142e047a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421, #5435) - Dev: Reduced the amount of scale events. (#5404, #5406) - Dev: Removed unused timegate settings. (#5361) +- Dev: Unsingletonize `Resources2`. (#5460) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) - Dev: Images are now loaded in worker threads. (#5431) - Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) diff --git a/cmake/resources/ResourcesAutogen.hpp.in b/cmake/resources/ResourcesAutogen.hpp.in index b047192d6a5..affafc3dba0 100644 --- a/cmake/resources/ResourcesAutogen.hpp.in +++ b/cmake/resources/ResourcesAutogen.hpp.in @@ -3,15 +3,14 @@ ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #include -#include "common/Singleton.hpp" namespace chatterino { -class Resources2 : public Singleton +class Resources2 { public: Resources2(); @RES_HEADER_CONTENT@ }; -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index 18995dc31da..7bafdd4fbca 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -2,10 +2,15 @@ #include "debug/AssertInGuiThread.hpp" -namespace chatterino { namespace { - static Resources2 *resources = nullptr; -} + +using namespace chatterino; + +static Resources2 *resources = nullptr; + +} // namespace + +namespace chatterino { Resources2 &getResources() { From f111b0f08d85a6c7ad6e7d214e004fcfedc12d33 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 16 Jun 2024 15:44:08 +0200 Subject: [PATCH 091/105] chore: unsingletonize SoundController (#5462) --- CHANGELOG.md | 1 + src/Application.cpp | 5 +++-- src/Application.hpp | 2 +- src/controllers/sound/ISoundController.hpp | 9 ++------- src/controllers/sound/MiniaudioBackend.cpp | 18 ++++++------------ src/controllers/sound/MiniaudioBackend.hpp | 2 -- 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413142e047a..2d799002344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Bugfix: Fixed message history occasionally not loading after a sleep. (#5457) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) +- Dev: Unsingletonize `ISoundController`. (#5462) - Dev: Use Qt's high DPI scaling. (#4868, #5400) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) diff --git a/src/Application.cpp b/src/Application.cpp index a3017302e8f..529b2481b44 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -136,7 +136,7 @@ Application::Application(Settings &_settings, const Paths &paths, , ffzBadges(&this->emplace()) , seventvBadges(&this->emplace()) , userData(new UserDataController(paths)) - , sound(&this->emplace(makeSoundController(_settings))) + , sound(makeSoundController(_settings)) , twitchLiveController(&this->emplace()) , twitchPubSub(new PubSub(TWITCH_PUBSUB_URL)) , twitchBadges(new TwitchBadges) @@ -173,6 +173,7 @@ void Application::fakeDtor() this->seventvEmotes.reset(); // this->twitch.reset(); this->fonts.reset(); + this->sound.reset(); this->userData.reset(); } @@ -435,7 +436,7 @@ ISoundController *Application::getSound() { assertInGuiThread(); - return this->sound; + return this->sound.get(); } ITwitchLiveController *Application::getTwitchLiveController() diff --git a/src/Application.hpp b/src/Application.hpp index 4cce1aa8ec6..af1c563a8e6 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -161,7 +161,7 @@ class Application : public IApplication FfzBadges *const ffzBadges{}; SeventvBadges *const seventvBadges{}; std::unique_ptr userData; - ISoundController *const sound{}; + std::unique_ptr sound; TwitchLiveController *const twitchLiveController{}; std::unique_ptr twitchPubSub; std::unique_ptr twitchBadges; diff --git a/src/controllers/sound/ISoundController.hpp b/src/controllers/sound/ISoundController.hpp index ebf7e3425b6..10e8c6c7332 100644 --- a/src/controllers/sound/ISoundController.hpp +++ b/src/controllers/sound/ISoundController.hpp @@ -1,14 +1,9 @@ #pragma once -#include "common/Singleton.hpp" - #include namespace chatterino { -class Settings; -class Paths; - enum class SoundBackend { Miniaudio, Null, @@ -17,11 +12,11 @@ enum class SoundBackend { /** * @brief Handles sound loading & playback **/ -class ISoundController : public Singleton +class ISoundController { public: ISoundController() = default; - ~ISoundController() override = default; + virtual ~ISoundController() = default; ISoundController(const ISoundController &) = delete; ISoundController(ISoundController &&) = delete; ISoundController &operator=(const ISoundController &) = delete; diff --git a/src/controllers/sound/MiniaudioBackend.cpp b/src/controllers/sound/MiniaudioBackend.cpp index f84ee8991c2..63b7efcf0c5 100644 --- a/src/controllers/sound/MiniaudioBackend.cpp +++ b/src/controllers/sound/MiniaudioBackend.cpp @@ -68,10 +68,13 @@ namespace chatterino { // NUM_SOUNDS specifies how many simultaneous default ping sounds & decoders to create constexpr const auto NUM_SOUNDS = 4; -void MiniaudioBackend::initialize(Settings &settings, const Paths &paths) +MiniaudioBackend::MiniaudioBackend() + : context(std::make_unique()) + , engine(std::make_unique()) + , workGuard(boost::asio::make_work_guard(this->ioContext)) + , sleepTimer(this->ioContext) { - (void)(settings); - (void)(paths); + qCInfo(chatterinoSound) << "Initializing miniaudio sound backend"; boost::asio::post(this->ioContext, [this] { ma_result result{}; @@ -192,15 +195,6 @@ void MiniaudioBackend::initialize(Settings &settings, const Paths &paths) }); } -MiniaudioBackend::MiniaudioBackend() - : context(std::make_unique()) - , engine(std::make_unique()) - , workGuard(boost::asio::make_work_guard(this->ioContext)) - , sleepTimer(this->ioContext) -{ - qCInfo(chatterinoSound) << "Initializing miniaudio sound backend"; -} - MiniaudioBackend::~MiniaudioBackend() { // NOTE: This destructor is never called because the `runGui` function calls _exit before that happens diff --git a/src/controllers/sound/MiniaudioBackend.hpp b/src/controllers/sound/MiniaudioBackend.hpp index 18ef9ed00e3..b8f359c3ce7 100644 --- a/src/controllers/sound/MiniaudioBackend.hpp +++ b/src/controllers/sound/MiniaudioBackend.hpp @@ -25,8 +25,6 @@ namespace chatterino { **/ class MiniaudioBackend : public ISoundController { - void initialize(Settings &settings, const Paths &paths) override; - public: MiniaudioBackend(); ~MiniaudioBackend() override; From b83d8ec9e333d43e15425e4768eeef319ba1e6ce Mon Sep 17 00:00:00 2001 From: Jacob Nielsen <3404570+Niller2005@users.noreply.github.com> Date: Sun, 16 Jun 2024 18:07:28 +0200 Subject: [PATCH 092/105] Add niller2005 to contributors list (#5464) --- resources/avatars/niller2005.png | Bin 0 -> 5414 bytes resources/contributors.txt | 1 + 2 files changed, 1 insertion(+) create mode 100644 resources/avatars/niller2005.png diff --git a/resources/avatars/niller2005.png b/resources/avatars/niller2005.png new file mode 100644 index 0000000000000000000000000000000000000000..1a2b5bab1282e831c4718b3c26ec020591f164fb GIT binary patch literal 5414 zcmV+>71`>EP)djAK8~p=((&1G0iX$d*ikQXP9h7>7zAhe;}zST(K^crhB!xd8fj%ROMk|a$Du_NMjXfxX zJt>4sGkHNNeL*5;FAI`nIJa<9oDYT12L3POfA}s%S>7bXA~hPRO8ow}@udv6QBHSmCscU0gwRa7$A<6lYR0 zf@@5dglG2W$NTHd|LM#3;ltL#r0LYQ$+VWaqlTxFcTP%4aAaUlI3|^fh_$W8x5VnV z!1KYp){}*jD;XF-EG$4F7*j4HUo0PHF&bPb5>+h`U^g0XLnV@4MC84q{K8Ag$h`l} zLH+2_`RvmA-L(ABsr|{D`NECuw}0`tclWk=_P2rdwukh!js3@?%$axH!It{!#Qo*L z{@=m*(X`E=hpl{Os)cHOQ8br7ErCi1)|FL&j&iNv)l*$9TTvl%F#?rYA#W}QaW4~S zCJb#V7<4BS8b5OtMQ<5OYaL2rF;{D6YI`wxh*q)3R+*k_yTFar+OzWY*ZcPJ{P*+s z=FH{ls>#xXp`>+}lUI6esJE(rx2KNf)PV8sb=~Dv+~-5S&_12DDSetDV0|WCa4%kD zIB;`4Y-U4ZQ$txhHa#A|t!T8QapApy?ZRuGicIj%cks?>@XlW4(RqiCQ%`(ZW|DHj z!H(0wapSyN_{2)~!ch0aS@pnR^0#L9xNi8bYWt>U_KaKYY+CbVSm|p|>3dN4mR##{ zOzx0K{k~ZFyJ7dQSop7F{-#;^jAz`jJ?OSO=(I!6oGa(CNVS6_&z(N#vQzxYXZ^`n z|H@GR%SrpqN&3xD_smxI%3b!$XXm_frhG<7Ap}ny1wa!BwtEYV0000GbW%=J|NsC0 z|NsC0|NsC0|Ns90;3Hib000riNkl5d6bsrea7+cbwAHL3XP<#4pMUOu-g8~=@BW?hJdZxBA2MW>Hvt@`QrlW!77c{Y z)ft!wBw+g7gzx>_(((=5ZUD4_^(I&I390V-Cm51G$#5H2_U4z@?bw~01vF?{Xha65 z83ILmMfBxwtl@SBtOu+MC+&U3trmjr(%Yg74q&y>+464bTA4@}uH!8j&aiGmOk=*}t7plvQs1&v9DK z@e8zZ-0a!Ace{ZBKIK2gPJk0}2xh>I5dW@BfQJP|ot71%D}Vmu!XrLzLCD_T`^>Is zhynKg8QZU4Z!??#{j!nalC+4iF$%}|sqGc?N*Tt>Z+WG(44)Fzi0RibbJ^tx!1Ocn zy-Q{?jAPN!{ikJuJGs4b>jmR~cQsZ4AW?FIQuOC1cn$jBTbO#7u==y z^?(6eTYvNojh-M**j45K1-W^uFnx^RKQ1q_Aj5(*;X1tJTrfkT_Gz|2tOw>ReCUOz zHL~E{SwDO)9>cH^WK(v|tq+93JOTG*=T+X0*1waCjY#VHbtJHH}UN^LZ!?X77 zp3q*}W{$&8UQzS~!GoBz;F~;VUdr#|%AhaB5a^{P^_>$aY4==4F@U%GzB}w2Z_MRF zk+@%t@cY?6ztkzc#?|e12{6|jhY3&WihFKF;QZ&^{>%*eWui|YLr$qRXn_Gy<6AbX z6j=TJH}Cqafi@dJ2s6kakd{xlCH%v|_4oc{dgDwbhRBeFEXmSPeLh<=7iU^X4YEI; z0&E<2=T$%}z+6Ck7*578XYa1F6gxh9-jb+U9EZa$_w2m?ORqrS^qm=r#p@~HXsOKz zBLREFvG%C>$CnqyFCuX)S!1g6J1gh4K4H_$IG-Q3YQB^Lq4;_Y<(vxQIE2f##{-RX zY={76NMP6bKig3m-^GT>?EZrn7F*Z>SS#^_q!O58cJF?R;}BROoiQtV!v-L;8E#k6 zWj*uXZOa$3STIazZgH_i79jW7^#TPow#D6@F@$!&9EXxL4Qzl7LW~epxH&rO#-L0f zy<7;jBEZ#3!9V!HTwi$BITxPOmj6(O8O=a~v#8qviIH}6ABnaB8v&?UUCuWIR&2W7 zTY%-;Ykbe+PZjQYQ9knz#BmrBGI|>r!LG3}eN11&SbK&_FM0lkXP4Q<-}vZiwG+#P zbg`bT^4{UUM&P*yVZbBP_n-PkYc#QWd|wayMa{z_-16{-0d#OyUqkvO>qb zM_&^_8^^F2NzANS>gHy*oPtYEm}n*u0LYSGhlbM=@ygiZkAVO%HgUmg7O1oYe!stx zp4HT8OxJWbnHUcV2@E0JCF5`DV}OB^n(b!5mCQx-DW_j%+l*jRSP zp+g8D1_@tW{HI{-i2%(s8+_5PO}~0zmihnL`DX<-I5ss&nlf2(2ax`mJ!kuQ2c`fM z05C2iOZXo^tqj=5NJfJ@=61&kAKnZ!VN4Tc$V{U%nHQJ<7nPTfA3tG&1On4!EV}6> zNz5&Wxqy$q@C``1RDdyD?Z(EY=9Y_>F9Wr-|DgJdEfWHOkU|qgm4$!!{_bcAER4(R z`*$IF86=1xK&ZB7sUI>#xMuLdvQ-NM1~ZtiCazh7;rYFQjRB}od3ia@OfISpcPJIPKj2WctgoT@v|6b~c?P~;}t;_#n z0R+5)2gV+jbVyQTv;o9zW;ip-=mutXZ`bn6wRYV)C5f_ZtF|qOwwegvt-rb(5Nh-_ zMvoBIJ{L-`5p(tk%@o54>SmK_FEb@!07DXXxcDsH54T2dBkKJ^I^3y;YV`RGL|;A% z`otGBfnH`>-~gV@5Q%8C@06r*wgm1k0BMH-kf1Uz(v-2C>mGXLu?uW`4T#?S*Cs}b z!OfI4<8%)dfaz5~yPGL8Vu-<7AOpg+r+2kc64cruIv(GrM+MP{AbsNIiI08;(M!`3 z49y0Z1^Z@`vS(_r^;TJsOfQodKzk~vtMERR9=Uc?mr*m&{zS5U%#G)g3KzEh?U$Cn zoNQc4Kf#QqsliCiltI%}6f+La9kFLaf&pQG>`1o_Ld}>CuD$poFZ-xVb>t>ry!G@w zW{3bN0~)CA~Gi28OI@C0s=@E_sYB)`G%tWl$ z;Qt>jh6tc$P)AXo30Ha93V`vrmElyniNFz^u$?hBfB}IS^v#>I z%m(WK?L{V#N!=Lm1h4I{r-=z&+TH&TE_rg2V{-HI#;)JbJSO(JQR+GFC*}sFe5ay`zMqlyoGF&Ijf- zhy+j`TBq}=2Y=aI);Kd~BftG^a=D+c9j9z+BKv7%0KydqN7I%U2jTmW)umK4JaDa z-8H?X?Ut`Uf5n;gi3B(@RA4Y6vjI@+sUN=bsN{BZ(nL)P!KDl%0E8d~!0(}Z%GB@v z{dd07^|3i&r-o0e-Kiru*Rz&oMwn@L2&Q-Ch!`?Zs}Q4{+JaIb14tkcae-FGc04mf z36Vaj^O``rd(1L}`B$Oo(3M00WN|zuEhNM8WrWj0sot5mWtDQa=%p3}xP2YVi2qpO zb}50(0w^Mig5C{c$O3?!A83FaWW{umAo@iLZoBnc|DIIFfeA2xmx=7Lv_3~k5)8oL zC1dr5ayBmkPKmKHLqJ9s00vM53Jw$R34dDhV$)0k=x}lb0&S1)sL7_;KA?%F2%^9t z2B4?)gCBk=a2h8^r~wv2^gnT0ihp$)z`=Jo+2oPP1K}OC{+q9lu|drMYG3C<#1Y;* zpa=)Z6tY0il)cyK^$airQnxx1F$bm+ppu;&LOxg;K}yLE`vmW_-CLkPllCx4N-;RE z2q$#WD|b>N#+*}aff-cwvb$PKIK)YCco761UF{eARxCk9Ks}^g0^Dn&3ZQ2=IVqn$AiSJHtF1=#A z)J(^<;gfeT$>ahc?e%*A5)^b+fn;7&6eFmr@M8W|M@RZ)B;aK3Nm3U7Xk(Ay-IALQ zs-^vpx^I85wG>M^2zw|90MjcgzPdKK;59Rn7H%^yMKZAx0buxE!r?E!fr+RE5J9Ju zrfsDu%22Uc6do3U34m~nVV!<;!K)Q@jI{8j^TO9-AYHSOHMKzc{_puy-lRiQ&cqlB zz}snB+-X8g1n=eFJB2K9A|aq$Ecu>!BWI4aNR3IByZ!Fpob6hmbzfB1oUdl%)t2rg#INn!PwXY8=;j6tQzYk^)#qCh{}OHwcZ5(RoiPW;uP z`G0JP8S@vJaQ2h~x6PdV_eD9l@2xeb{@O7P&ipO_X^QF7oeUML`c>zm86}IJ^us5? zrl&1-p}%3r>=VOJGdT3Q?@T2cgg^-ZlES&wnBfGjppL2+onZi6=s!Wiki{%s#^y_c z@g%!lF!_loF&aB!FnzN09CNI4sGw%?`w}6r3q#%5Qs!m8)V_rD#YI)3TSvwl00YEe z(J#q@IWCviSD(IM#~br(@-YAr0F#j0Go4ES2zpV#Wgk6~6ES862*wnU?6CM;j>`tQ zX2Q9jR4@w4GQhp^d)u!0h;5)pA3raOp4e0Dhvpr9dkQE3(Ri#vicbJABM5^wG@%_k5TO#oC>l+Uhq`ng5R zm-ZYvvUuszWuLj&h2H`)0DWz2CnO>SfTwAh0mAI}y6xx08V3LrM^E5fj6eluUsZKg zn~E?9Gr&kvI*y$SxcC_`z(+7!Ffm-yb>wVUBMWkQGg?kqP*X z`j^?ORVE^S#wGN!u?^IhLp>0plMoXkAkvt0ONd_e^fB+Mld)Yz1}LPN@z185mDiOJ zfcei|$a5?Oj4>rOW=KU{nsknF9>_VwkVT6jVj(~q0*OBMt|h+vllt~nb}p!8`X!q$ zgDx<*)UFo8rJ-wVRsax*m3dTw7lJ^viaujInRvGgt8{kZCYj zz-;zmi!>~}bEOx6!4GqG$|iu-8`o@``YnHL%thaRDt=v6)eLYFQwg9zK#3G>8!^&8 zKnnS%SM6VEKqAv;A4a7BD}IoKw{8U?gBr(vpqc=)RY%3XY>Z;T0wvKw!7+nGE-;K_ zs$O_;m189kMSzTl7h5+18@Y;cD42vukQ)dedWU_K*C8h&WSJ5vvlS#~wh}q7;d?&v z(#jc){M_+|1c<`ahDR^gb);S6y8i9jf;gR>5<&pqn)X$@+D=3vC=^19^MnYg6(cwe zSsZw!9rQ1JK<4DU2%iVQt_@8UXX*yD_~ou2!Dspk1bQA!ok=ds2rj6cdUDEV!7@NX zK|q3g`$E;7tG21-L;zXh287rd9=&q%hXeS9=qo?{X{fEe?;hFos#j1k})(!BkOARdr%zCJk~1Bu{z$;|Bmv=F^W`=mm`M$)Db&>*-~;Zqe%fz)YbTaMy)s zOg6g?KqP^fSw>a7Gb#KvU#-rf6eLH1lZE~TydW^>tNZ_&uP`5WMGq8&g89)!ikm;W z72(C_)x;Pwz<`p_X#nBpEX!hPE&?bMfRieq7Wr^s$@3T7{g?YH4c?{y1M=f8kL*e^ Q6#xJL07*qoM6N<$g02TA&Hw-a literal 0 HcmV?d00001 diff --git a/resources/contributors.txt b/resources/contributors.txt index 9d131c7b9ed..8fdda0e286b 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -77,6 +77,7 @@ SputNikPlop | https://github.com/SputNikPlop | fraxx | https://github.com/fraxxio | :/avatars/fraxx.png KleberPF | https://github.com/KleberPF | nealxm | https://github.com/nealxm | :/avatars/nealxm.png +Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png # If you are a contributor add yourself above this line From 052dbd7d2cc2a25d832e675443b68d0643cfdd9d Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 19 Jun 2024 14:46:54 +0200 Subject: [PATCH 093/105] fix: check state when completing commands (#5401) --- CHANGELOG.md | 1 + src/controllers/plugins/PluginController.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d799002344..141124c49dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Bugfix: Fixed restricted users usernames not being clickable. (#5405) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Bugfix: Fixed message history occasionally not loading after a sleep. (#5457) +- Bugfix: Fixed a crash when tab completing while having an invalid plugin loaded. (#5401) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Unsingletonize `ISoundController`. (#5462) diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 8c2d8055619..199e0f4e4d6 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -420,7 +420,7 @@ std::pair PluginController::updateCustomCompletions( for (const auto &[name, pl] : this->plugins()) { - if (!pl->error().isNull()) + if (!pl->error().isNull() || pl->state_ == nullptr) { continue; } From 7dc80bc599c985d45470f8c7f3b615d57f20878e Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 21 Jun 2024 14:48:51 +0200 Subject: [PATCH 094/105] chore: post some ssl info on startup (#5475) --- src/main.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 8da92a45ca6..0e4d4996871 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -90,6 +91,25 @@ int main(int argc, char **argv) attachToConsole(); } + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL library build version:" + << QSslSocket::sslLibraryBuildVersionString(); + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL library version:" + << QSslSocket::sslLibraryVersionString(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL active backend:" + << QSslSocket::activeBackend() << "of" + << QSslSocket::availableBackends().join(", "); +# if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + qCInfo(chatterinoApp) << "Chatterino Qt SSL active backend features:" + << QSslSocket::supportedFeatures(); +# endif + qCInfo(chatterinoApp) << "Chatterino Qt SSL active backend protocols:" + << QSslSocket::supportedProtocols(); +#endif + Updates updates(*paths); NetworkConfigurationProvider::applyFromEnv(Env::get()); From c980162656aa7d98ee1b11e050350c4d351ab6ca Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 22 Jun 2024 12:04:30 +0200 Subject: [PATCH 095/105] Introduce HTTP API for plugins (#5383) --- CHANGELOG.md | 1 + docs/chatterino.d.ts | 32 ++ docs/plugin-meta.lua | 84 +++- docs/wip-plugins.md | 150 ++++++ scripts/make_luals_meta.py | 1 - src/CMakeLists.txt | 4 + src/common/network/NetworkCommon.hpp | 3 + src/common/network/NetworkRequest.cpp | 7 + src/common/network/NetworkRequest.hpp | 2 + src/controllers/plugins/LuaAPI.hpp | 9 +- src/controllers/plugins/Plugin.cpp | 29 +- src/controllers/plugins/Plugin.hpp | 3 + src/controllers/plugins/PluginController.cpp | 12 + src/controllers/plugins/PluginPermission.cpp | 2 + src/controllers/plugins/PluginPermission.hpp | 1 + src/controllers/plugins/api/HTTPRequest.cpp | 451 +++++++++++++++++++ src/controllers/plugins/api/HTTPRequest.hpp | 162 +++++++ src/controllers/plugins/api/HTTPResponse.cpp | 144 ++++++ src/controllers/plugins/api/HTTPResponse.hpp | 80 ++++ 19 files changed, 1166 insertions(+), 11 deletions(-) create mode 100644 src/controllers/plugins/api/HTTPRequest.cpp create mode 100644 src/controllers/plugins/api/HTTPRequest.hpp create mode 100644 src/controllers/plugins/api/HTTPResponse.cpp create mode 100644 src/controllers/plugins/api/HTTPResponse.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 141124c49dc..314884c2647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Minor: Moderators can now see when users are warned. (#5441) - Minor: Added support for Brave & google-chrome-stable browsers. (#5452) - Minor: Added drop indicator line while dragging in tables. (#5256) +- Minor: Introduce HTTP API for plugins. (#5383) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 95d2282be9f..7929fbc3d23 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -32,6 +32,8 @@ declare module c2 { is_valid(): boolean; } + interface ISharedResource {} + class RoomModes { unique_chat: boolean; subscriber_only: boolean; @@ -69,6 +71,36 @@ declare module c2 { static by_twitch_id(id: string): null | Channel; } + enum HTTPMethod { + Get, + Post, + Put, + Delete, + Patch, + } + + class HTTPResponse implements ISharedResource { + data(): string; + status(): number | null; + error(): string; + } + + type HTTPCallback = (res: HTTPResponse) => void; + class HTTPRequest implements ISharedResource { + on_success(callback: HTTPCallback): void; + on_error(callback: HTTPCallback): void; + finally(callback: () => void): void; + + set_timeout(millis: number): void; + set_payload(data: string): void; + set_header(name: string, value: string): void; + + execute(): void; + + // might error + static create(method: HTTPMethod, url: string): HTTPRequest; + } + function log(level: LogLevel, ...data: any[]): void; function register_command( name: String, diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 391b5745d42..453e4f548c8 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -5,7 +5,6 @@ -- Add the folder this file is in to "Lua.workspace.library". c2 = {} - ---@alias c2.LogLevel integer ---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } c2.LogLevel = {} @@ -159,6 +158,89 @@ function c2.Channel.by_twitch_id(id) end -- End src/controllers/plugins/api/ChannelRef.hpp +-- Begin src/controllers/plugins/api/HTTPRequest.hpp + +---@class HTTPResponse +---@field data string Data received from the server +---@field status integer? HTTP Status code returned by the server +---@field error string A somewhat human readable description of an error if such happened + +---@alias HTTPCallback fun(result: HTTPResponse): nil +---@class HTTPRequest +HTTPRequest = {} + +--- Sets the success callback +--- +---@param callback HTTPCallback Function to call when the HTTP request succeeds +function HTTPRequest:on_success(callback) end + +--- Sets the failure callback +--- +---@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status +function HTTPRequest:on_error(callback) end + +--- Sets the finally callback +--- +---@param callback fun(): nil Function to call when the HTTP request finishes +function HTTPRequest:finally(callback) end + +--- Sets the timeout +--- +---@param timeout integer How long in milliseconds until the times out +function HTTPRequest:set_timeout(timeout) end + +--- Sets the request payload +--- +---@param data string +function HTTPRequest:set_payload(data) end + +--- Sets a header in the request +--- +---@param name string +---@param value string +function HTTPRequest:set_header(name, value) end + +--- Executes the HTTP request +--- +function HTTPRequest:execute() end + +--- Creates a new HTTPRequest +--- +---@param method HTTPMethod Method to use +---@param url string Where to send the request to +---@return HTTPRequest +function HTTPRequest.create(method, url) end + +-- End src/controllers/plugins/api/HTTPRequest.hpp + +-- Begin src/controllers/plugins/api/HTTPResponse.hpp + +---@class HTTPResponse +HTTPResponse = {} + +--- Returns the data. This is not guaranteed to be encoded using any +--- particular encoding scheme. It's just the bytes the server returned. +--- +function HTTPResponse:data() end + +--- Returns the status code. +--- +function HTTPResponse:status() end + +--- A somewhat human readable description of an error if such happened +--- +function HTTPResponse:error() end + +-- End src/controllers/plugins/api/HTTPResponse.hpp + +-- Begin src/common/network/NetworkCommon.hpp + +---@alias HTTPMethod integer +---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod } +HTTPMethod = {} + +-- End src/common/network/NetworkCommon.hpp + --- Registers a new command called `name` which when executed will call `handler`. --- ---@param name string The name of the command. diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 32eda387fd1..bf49e0f4c55 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -85,6 +85,24 @@ Example: } ``` +### Network + +Allows the plugin to send HTTP requests. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "Network" + }, + ... + ] +} +``` + ## Plugins with Typescript If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io) @@ -370,6 +388,138 @@ Returns `true` if the channel can be moderated by the current user. Returns `true` if the current user is a VIP in the channel. +#### `HTTPMethod` enum + +This table describes HTTP methods available to Lua Plugins. The values behind +the names may change, do not count on them. It has the following keys: + +- `Get` +- `Post` +- `Put` +- `Delete` +- `Patch` + +#### `HTTPResponse` + +An `HTTPResponse` is a table you receive in the callback after a completed `HTTPRequest`. + +##### `HTTPResponse.data()` + +This function returns the data received from the server as a string. Usually +this will be UTF-8-encoded however that is not guaranteed, this could be any +binary data. + +##### `HTTPResponse.error()` + +If an error happened this function returns a human readable description of it. + +It returns something like: `"ConnectionRefusedError"`, `"401"`. + +##### `HTTPResponse.status()` + +This function returns the HTTP status code of the request or `nil` if there was +an error before a status code could be received. + +```lua +{ + data = "This is the data received from the server as a string", + status = 200, -- HTTP status code returned by the server or nil if no response was received because of an error + error = "A somewhat human readable description of an error if such happened" +} +``` + +#### `HTTPRequest` + +Allows you to send an HTTP request to a URL. Do not create requests that you +don't want to call `execute()` on. For the time being that leaks callback +functions and all their upvalues with them. + +##### `HTTPRequest.create(method, url)` + +Creates a new `HTTPRequest`. The `method` argument is an +[`HTTPMethod`](#HTTPMethod-enum). The `url` argument must be a string +containing a valid URL (ex. `https://example.com/path/to/api`). + +```lua +local req = c2.HTTPRequest.create(c2.HTTPMethod.Get, "https://example.com") +req:on_success(function (res) + print(res.data) +end) +req:execute() +``` + +##### `HTTPRequest:on_success(callback)` + +Sets the success callback. It accepts a function that takes a single parameter +of type `HTTPResponse`. The callback will be called on success. This function +returns nothing. + +##### `HTTPRequest:on_error(callback)` + +Sets the error callback. It accepts a function that takes a single parameter of +type `HTTPResponse`. The callback will be called if the request fails. To see why +it failed check the `error` field of the result. This function returns nothing. + +##### `HTTPRequest:finally(callback)` + +Sets the finally callback. It accepts a function that takes no parameters and +returns nothing. It will be always called after `success` or `error`. This +function returns nothing. + +##### `HTTPRequest:set_timeout(timeout)` + +Sets how long the request will take before it times out. The `timeout` +parameter is in milliseconds. This function returns nothing. + +##### `HTTPRequest:set_payload(data)` + +Sets the data that will be sent with the request. The `data` parameter must be +a string. This function returns nothing. + +##### `HTTPRequest:set_header(name, value)` + +Adds or overwrites a header in the request. Both `name` and `value` should be +strings. If they are not strings they will be converted to strings. This +function returns nothing. + +##### `HTTPRequest:execute()` + +Sends the request. This function returns nothing. + +```lua +local url = "http://localhost:8080/thing" +local request = c2.HTTPRequest.create("Post", url) +request:set_timeout(1000) +request:set_payload("TEST!") +request:set_header("X-Test", "Testing!") +request:set_header("Content-Type", "text/plain") +request:on_success(function (res) + print('Success!') + -- Data is in res.data + print(res.status) +end) +request:on_error(function (res) + print('Error!') + print(res.status) + print(res.error) +end) +request:finally(function () + print('Finally') +end) +request:execute() + +-- This prints: +-- Success! +-- [content of /thing] +-- 200 +-- Finally + +-- Or: +-- Error! +-- nil +-- ConnectionRefusedError +``` + ### Input/Output API These functions are wrappers for Lua's I/O library. Functions on file pointer diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index adbbeb9dfac..f9f44e7ed20 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -41,7 +41,6 @@ -- Add the folder this file is in to "Lua.workspace.library". c2 = {} - """ repo_root = Path(__file__).parent.parent diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 82634375545..4a7bcde6703 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -227,6 +227,10 @@ set(SOURCE_FILES controllers/plugins/api/ChannelRef.hpp controllers/plugins/api/IOWrapper.cpp controllers/plugins/api/IOWrapper.hpp + controllers/plugins/api/HTTPRequest.cpp + controllers/plugins/api/HTTPRequest.hpp + controllers/plugins/api/HTTPResponse.cpp + controllers/plugins/api/HTTPResponse.hpp controllers/plugins/LuaAPI.cpp controllers/plugins/LuaAPI.hpp controllers/plugins/PluginPermission.cpp diff --git a/src/common/network/NetworkCommon.hpp b/src/common/network/NetworkCommon.hpp index a5a44430e95..215b828a75f 100644 --- a/src/common/network/NetworkCommon.hpp +++ b/src/common/network/NetworkCommon.hpp @@ -15,6 +15,9 @@ using NetworkSuccessCallback = std::function; using NetworkErrorCallback = std::function; using NetworkFinallyCallback = std::function; +/** + * @exposeenum HTTPMethod + */ enum class NetworkRequestType { Get, Post, diff --git a/src/common/network/NetworkRequest.cpp b/src/common/network/NetworkRequest.cpp index b436216ae06..f46da079a07 100644 --- a/src/common/network/NetworkRequest.cpp +++ b/src/common/network/NetworkRequest.cpp @@ -98,6 +98,13 @@ NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header, return std::move(*this); } +NetworkRequest NetworkRequest::header(const QByteArray &headerName, + const QByteArray &value) && +{ + this->data->request.setRawHeader(headerName, value); + return std::move(*this); +} + NetworkRequest NetworkRequest::headerList( const std::vector> &headers) && { diff --git a/src/common/network/NetworkRequest.hpp b/src/common/network/NetworkRequest.hpp index 4da4c7a9e9e..1308fb023b1 100644 --- a/src/common/network/NetworkRequest.hpp +++ b/src/common/network/NetworkRequest.hpp @@ -57,6 +57,8 @@ class NetworkRequest final NetworkRequest header(const char *headerName, const char *value) &&; NetworkRequest header(const char *headerName, const QByteArray &value) &&; NetworkRequest header(const char *headerName, const QString &value) &&; + NetworkRequest header(const QByteArray &headerName, + const QByteArray &value) &&; NetworkRequest header(QNetworkRequest::KnownHeaders header, const QVariant &value) &&; NetworkRequest headerList( diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index fd2c12e4144..dd3ee83062b 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -82,6 +82,9 @@ struct CompletionEvent { /** * @includefile common/Channel.hpp * @includefile controllers/plugins/api/ChannelRef.hpp + * @includefile controllers/plugins/api/HTTPRequest.hpp + * @includefile controllers/plugins/api/HTTPResponse.hpp + * @includefile common/network/NetworkCommon.hpp */ /** @@ -133,7 +136,11 @@ int searcherRelative(lua_State *L); // This is a fat pointer that allows us to type check values given to functions needing a userdata. // Ensure ALL userdata given to Lua are a subclass of this! Otherwise we garbage as a pointer! struct UserData { - enum class Type { Channel }; + enum class Type { + Channel, + HTTPRequest, + HTTPResponse, + }; Type type; bool isWeak; }; diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index 4609fee7c17..4a8f89f0912 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -1,8 +1,10 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/Plugin.hpp" +# include "common/network/NetworkCommon.hpp" # include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/PluginPermission.hpp" # include "util/QMagicEnum.hpp" extern "C" { @@ -11,6 +13,8 @@ extern "C" { # include # include # include +# include +# include # include # include @@ -258,16 +262,25 @@ bool Plugin::hasFSPermissionFor(bool write, const QString &path) using PType = PluginPermission::Type; auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead; - // XXX: Older compilers don't have support for std::ranges - // NOLINTNEXTLINE(readability-use-anyofallof) - for (const auto &p : this->meta.permissions) + return std::ranges::any_of(this->meta.permissions, [=](const auto &p) { + return p.type == typ; + }); +} + +bool Plugin::hasHTTPPermissionFor(const QUrl &url) +{ + auto proto = url.scheme(); + if (proto != "http" && proto != "https") { - if (p.type == typ) - { - return true; - } + qCWarning(chatterinoLua).nospace() + << "Plugin " << this->id << " (" << this->meta.name + << ") is trying to use a non-http protocol"; + return false; } - return false; + + return std::ranges::any_of(this->meta.permissions, [](const auto &p) { + return p.type == PluginPermission::Type::Network; + }); } } // namespace chatterino diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 2adbe9067fb..f8375247f5d 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -2,12 +2,14 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "Application.hpp" +# include "common/network/NetworkCommon.hpp" # include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/PluginPermission.hpp" # include # include +# include # include # include @@ -139,6 +141,7 @@ class Plugin void removeTimeout(QTimer *timer); bool hasFSPermissionFor(bool write, const QString &path); + bool hasHTTPPermissionFor(const QUrl &url); private: QDir loadDirectory_; diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 199e0f4e4d6..2fa2865f4eb 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -3,10 +3,13 @@ # include "Application.hpp" # include "common/Args.hpp" +# include "common/network/NetworkCommon.hpp" # include "common/QLogging.hpp" # include "controllers/commands/CommandContext.hpp" # include "controllers/commands/CommandController.hpp" # include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/api/HTTPRequest.hpp" +# include "controllers/plugins/api/HTTPResponse.hpp" # include "controllers/plugins/api/IOWrapper.hpp" # include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" @@ -174,10 +177,19 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, lua::pushEnumTable(L); lua_setfield(L, c2libIdx, "ChannelType"); + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "HTTPMethod"); + // Initialize metatables for objects lua::api::ChannelRef::createMetatable(L); lua_setfield(L, c2libIdx, "Channel"); + lua::api::HTTPRequest::createMetatable(L); + lua_setfield(L, c2libIdx, "HTTPRequest"); + + lua::api::HTTPResponse::createMetatable(L); + lua_setfield(L, c2libIdx, "HTTPResponse"); + lua_setfield(L, gtable, "c2"); // ban functions diff --git a/src/controllers/plugins/PluginPermission.cpp b/src/controllers/plugins/PluginPermission.cpp index 09204f93df4..9a4bc925e9c 100644 --- a/src/controllers/plugins/PluginPermission.cpp +++ b/src/controllers/plugins/PluginPermission.cpp @@ -37,6 +37,8 @@ QString PluginPermission::toHtml() const return "Read files in its data directory"; case PluginPermission::Type::FilesystemWrite: return "Write to or create files in its data directory"; + case PluginPermission::Type::Network: + return "Make requests over the internet to third party websites"; default: assert(false && "invalid PluginPermission type in toHtml()"); return "shut up compiler, this never happens"; diff --git a/src/controllers/plugins/PluginPermission.hpp b/src/controllers/plugins/PluginPermission.hpp index 5867b7b63d6..ffa728e7b43 100644 --- a/src/controllers/plugins/PluginPermission.hpp +++ b/src/controllers/plugins/PluginPermission.hpp @@ -14,6 +14,7 @@ struct PluginPermission { enum class Type { FilesystemRead, FilesystemWrite, + Network, }; Type type; std::vector errors; diff --git a/src/controllers/plugins/api/HTTPRequest.cpp b/src/controllers/plugins/api/HTTPRequest.cpp new file mode 100644 index 00000000000..6defabb38d4 --- /dev/null +++ b/src/controllers/plugins/api/HTTPRequest.cpp @@ -0,0 +1,451 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/HTTPRequest.hpp" + +# include "Application.hpp" +# include "common/network/NetworkCommon.hpp" +# include "common/network/NetworkRequest.hpp" +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/api/HTTPResponse.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "util/DebugCount.hpp" + +extern "C" { +# include +# include +} +# include +# include + +# include +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(*vararg) +// NOLINTNEXTLINE(*-avoid-c-arrays) +static const luaL_Reg HTTP_REQUEST_METHODS[] = { + {"on_success", &HTTPRequest::on_success_wrap}, + {"on_error", &HTTPRequest::on_error_wrap}, + {"finally", &HTTPRequest::finally_wrap}, + + {"execute", &HTTPRequest::execute_wrap}, + {"set_timeout", &HTTPRequest::set_timeout_wrap}, + {"set_payload", &HTTPRequest::set_payload_wrap}, + {"set_header", &HTTPRequest::set_header_wrap}, + // static + {"create", &HTTPRequest::create}, + {nullptr, nullptr}, +}; + +std::shared_ptr HTTPRequest::getOrError(lua_State *L, + StackIdx where) +{ + if (lua_gettop(L) < 1) + { + // The nullptr is there just to appease the compiler, luaL_error is no return + luaL_error(L, "Called c2.HTTPRequest method without a request object"); + return nullptr; + } + if (lua_isuserdata(L, where) == 0) + { + luaL_error( + L, + "Called c2.HTTPRequest method with a non-userdata 'self' argument"); + return nullptr; + } + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest"); + auto *data = + SharedPtrUserData::from( + checked); + if (data == nullptr) + { + luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer"); + return nullptr; + } + lua_remove(L, where); + if (data->target == nullptr) + { + luaL_error( + L, "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); + return nullptr; + } + if (data->target->done) + { + luaL_error(L, "This c2.HTTPRequest has already been executed!"); + return nullptr; + } + return data->target; +} + +void HTTPRequest::createMetatable(lua_State *L) +{ + lua::StackGuard guard(L, 1); + + luaL_newmetatable(L, "c2.HTTPRequest"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); // clone metatable + lua_settable(L, -3); // metatable.__index = metatable + + // Generic ISharedResource stuff + lua_pushstring(L, "__gc"); + lua_pushcfunction(L, (&SharedPtrUserData::destroy)); + lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy + + luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0); +} + +int HTTPRequest::on_success_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->on_success(L); +} + +int HTTPRequest::on_success(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:on_success needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error( + L, "HTTPRequest:on_success needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "success"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::on_error_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->on_error(L); +} + +int HTTPRequest::on_error(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:on_error needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error( + L, "HTTPRequest:on_error needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "error"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::set_timeout_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_timeout(L); +} + +int HTTPRequest::set_timeout(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:set_timeout needs 1 argument (a number of " + "milliseconds after which the request will time out)"); + } + + int temporary = -1; + if (!lua::pop(L, &temporary)) + { + return luaL_error( + L, "HTTPRequest:set_timeout failed to get timeout, expected a " + "positive integer"); + } + if (temporary <= 0) + { + return luaL_error( + L, "HTTPRequest:set_timeout failed to get timeout, expected a " + "positive integer"); + } + this->timeout_ = temporary; + return 0; +} + +int HTTPRequest::finally_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->finally(L); +} + +int HTTPRequest::finally(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " + "that takes nothing and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " + "that takes nothing and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "finally"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::set_payload_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_payload(L); +} + +int HTTPRequest::set_payload(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:set_payload needs 1 argument (a string payload)"); + } + + std::string temporary; + if (!lua::pop(L, &temporary)) + { + return luaL_error( + L, "HTTPRequest:set_payload failed to get payload, expected a " + "string"); + } + this->req_ = + std::move(this->req_).payload(QByteArray::fromStdString(temporary)); + return 0; +} + +int HTTPRequest::set_header_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -3); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_header(L); +} + +int HTTPRequest::set_header(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 2) + { + return luaL_error( + L, "HTTPRequest:set_header needs 2 arguments (a header name " + "and a value)"); + } + + std::string value; + if (!lua::pop(L, &value)) + { + return luaL_error( + L, "cannot get value (2nd argument of HTTPRequest:set_header)"); + } + std::string name; + if (!lua::pop(L, &name)) + { + return luaL_error( + L, "cannot get name (1st argument of HTTPRequest:set_header)"); + } + this->req_ = std::move(this->req_) + .header(QByteArray::fromStdString(name), + QByteArray::fromStdString(value)); + return 0; +} + +int HTTPRequest::create(lua_State *L) +{ + lua::StackGuard guard(L, -1); + if (lua_gettop(L) != 2) + { + return luaL_error( + L, "HTTPRequest.create needs exactly 2 arguments (method " + "and url)"); + } + QString url; + if (!lua::pop(L, &url)) + { + return luaL_error(L, + "cannot get url (2nd argument of HTTPRequest.create, " + "expected a string)"); + } + auto parsedurl = QUrl(url); + if (!parsedurl.isValid()) + { + return luaL_error( + L, "cannot parse url (2nd argument of HTTPRequest.create, " + "got invalid url in argument)"); + } + NetworkRequestType method{}; + if (!lua::pop(L, &method)) + { + return luaL_error( + L, "cannot get method (1st argument of HTTPRequest.create, " + "expected a string)"); + } + auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L); + if (!pl->hasHTTPPermissionFor(parsedurl)) + { + return luaL_error( + L, "Plugin does not have permission to send HTTP requests " + "to this URL"); + } + NetworkRequest r(parsedurl, method); + lua::push( + L, std::make_shared(ConstructorAccessTag{}, std::move(r))); + return 1; +} + +int HTTPRequest::execute_wrap(lua_State *L) +{ + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->execute(L); +} + +int HTTPRequest::execute(lua_State *L) +{ + auto shared = this->shared_from_this(); + this->done = true; + std::move(this->req_) + .onSuccess([shared, L](const NetworkResult &res) { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "success"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + lua::push(thread, std::make_shared(res)); + // one arg, no return, no msgh + lua_pcall(thread, 1, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + }) + .onError([shared, L](const NetworkResult &res) { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "error"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + lua::push(thread, std::make_shared(res)); + // one arg, no return, no msgh + lua_pcall(thread, 1, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + }) + .finally([shared, L]() { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "finally"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + // no args, no return, no msgh + lua_pcall(thread, 0, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + // remove our private data + lua_pushnil(thread); + lua_setfield(thread, LUA_REGISTRYINDEX, + shared->privateKey.toStdString().c_str()); + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + + // we removed our private table, forget the key for it + shared->privateKey = QString(); + }) + .timeout(this->timeout_) + .execute(); + return 0; +} + +HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/, + NetworkRequest req) + : req_(std::move(req)) +{ + DebugCount::increase("lua::api::HTTPRequest"); +} + +HTTPRequest::~HTTPRequest() +{ + DebugCount::decrease("lua::api::HTTPRequest"); + // We might leak a Lua function or two here if the request isn't executed + // but that's better than accessing a possibly invalid lua_State pointer. +} + +StackIdx HTTPRequest::pushPrivate(lua_State *L) +{ + if (this->privateKey.isEmpty()) + { + this->privateKey = QString("HTTPRequestPrivate%1") + .arg(QRandomGenerator::system()->generate()); + pushEmptyTable(L, 4); + lua_setfield(L, LUA_REGISTRYINDEX, + this->privateKey.toStdString().c_str()); + } + lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str()); + return lua_gettop(L); +} + +// NOLINTEND(*vararg) +} // namespace chatterino::lua::api + +namespace chatterino::lua { + +StackIdx push(lua_State *L, std::shared_ptr request) +{ + using namespace chatterino::lua::api; + + SharedPtrUserData::create( + L, std::move(request)); + luaL_getmetatable(L, "c2.HTTPRequest"); + lua_setmetatable(L, -2); + return lua_gettop(L); +} +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/HTTPRequest.hpp b/src/controllers/plugins/api/HTTPRequest.hpp new file mode 100644 index 00000000000..c373dcec14b --- /dev/null +++ b/src/controllers/plugins/api/HTTPRequest.hpp @@ -0,0 +1,162 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "common/network/NetworkRequest.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" + +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) + +/** + * @lua@class HTTPResponse + * @lua@field data string Data received from the server + * @lua@field status integer? HTTP Status code returned by the server + * @lua@field error string A somewhat human readable description of an error if such happened + */ + +/** + * @lua@alias HTTPCallback fun(result: HTTPResponse): nil + */ + +/** + * @lua@class HTTPRequest + */ +class HTTPRequest : public std::enable_shared_from_this +{ + // This type is private to prevent the accidental construction of HTTPRequest without a shared pointer + struct ConstructorAccessTag { + }; + +public: + HTTPRequest(HTTPRequest::ConstructorAccessTag, NetworkRequest req); + HTTPRequest(HTTPRequest &&other) = default; + HTTPRequest &operator=(HTTPRequest &&) = default; + HTTPRequest &operator=(HTTPRequest &) = delete; + HTTPRequest(const HTTPRequest &other) = delete; + ~HTTPRequest(); + +private: + NetworkRequest req_; + + static void createMetatable(lua_State *L); + friend class chatterino::PluginController; + + /** + * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest + * + * If the object given is not a userdatum or the pointer inside that + * userdatum doesn't point to a HTTPRequest, a lua error is thrown. + * + * This function always returns a non-null pointer. + */ + static std::shared_ptr getOrError(lua_State *L, + StackIdx where = -1); + /** + * Pushes the private table onto the lua stack. + * + * This might create it if it doesn't exist. + */ + StackIdx pushPrivate(lua_State *L); + + // This is the key in the registry the private table it held at (if it exists) + // This might be a null QString if the request has already been executed or + // the table wasn't created yet. + QString privateKey; + int timeout_ = 10'000; + bool done = false; + +public: + // These functions are wrapped so data can be accessed more easily. When a call from Lua comes in: + // - the static wrapper function is called + // - it calls getOrError + // - and then the wrapped method + + /** + * Sets the success callback + * + * @lua@param callback HTTPCallback Function to call when the HTTP request succeeds + * @exposed HTTPRequest:on_success + */ + static int on_success_wrap(lua_State *L); + int on_success(lua_State *L); + + /** + * Sets the failure callback + * + * @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status + * @exposed HTTPRequest:on_error + */ + static int on_error_wrap(lua_State *L); + int on_error(lua_State *L); + + /** + * Sets the finally callback + * + * @lua@param callback fun(): nil Function to call when the HTTP request finishes + * @exposed HTTPRequest:finally + */ + static int finally_wrap(lua_State *L); + int finally(lua_State *L); + + /** + * Sets the timeout + * + * @lua@param timeout integer How long in milliseconds until the times out + * @exposed HTTPRequest:set_timeout + */ + static int set_timeout_wrap(lua_State *L); + int set_timeout(lua_State *L); + + /** + * Sets the request payload + * + * @lua@param data string + * @exposed HTTPRequest:set_payload + */ + static int set_payload_wrap(lua_State *L); + int set_payload(lua_State *L); + + /** + * Sets a header in the request + * + * @lua@param name string + * @lua@param value string + * @exposed HTTPRequest:set_header + */ + static int set_header_wrap(lua_State *L); + int set_header(lua_State *L); + + /** + * Executes the HTTP request + * + * @exposed HTTPRequest:execute + */ + static int execute_wrap(lua_State *L); + int execute(lua_State *L); + + /** + * Static functions + */ + + /** + * Creates a new HTTPRequest + * + * @lua@param method HTTPMethod Method to use + * @lua@param url string Where to send the request to + * + * @lua@return HTTPRequest + * @exposed HTTPRequest.create + */ + static int create(lua_State *L); +}; + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api + +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request); +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/api/HTTPResponse.cpp b/src/controllers/plugins/api/HTTPResponse.cpp new file mode 100644 index 00000000000..f6d6ea1dfe4 --- /dev/null +++ b/src/controllers/plugins/api/HTTPResponse.cpp @@ -0,0 +1,144 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/HTTPResponse.hpp" + +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "util/DebugCount.hpp" + +extern "C" { +# include +} +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(*vararg) +// NOLINTNEXTLINE(*-avoid-c-arrays) +static const luaL_Reg HTTP_RESPONSE_METHODS[] = { + {"data", &HTTPResponse::data_wrap}, + {"status", &HTTPResponse::status_wrap}, + {"error", &HTTPResponse::error_wrap}, + {nullptr, nullptr}, +}; + +void HTTPResponse::createMetatable(lua_State *L) +{ + lua::StackGuard guard(L, 1); + + luaL_newmetatable(L, "c2.HTTPResponse"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); // clone metatable + lua_settable(L, -3); // metatable.__index = metatable + + // Generic ISharedResource stuff + lua_pushstring(L, "__gc"); + lua_pushcfunction(L, (&SharedPtrUserData::destroy)); + lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy + + luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0); +} + +std::shared_ptr HTTPResponse::getOrError(lua_State *L, + StackIdx where) +{ + if (lua_gettop(L) < 1) + { + // The nullptr is there just to appease the compiler, luaL_error is no return + luaL_error(L, "Called c2.HTTPResponse method without a request object"); + return nullptr; + } + if (lua_isuserdata(L, where) == 0) + { + luaL_error(L, "Called c2.HTTPResponse method with a non-userdata " + "'self' argument"); + return nullptr; + } + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse"); + auto *data = + SharedPtrUserData::from( + checked); + if (data == nullptr) + { + luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer"); + return nullptr; + } + lua_remove(L, where); + if (data->target == nullptr) + { + luaL_error( + L, + "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); + return nullptr; + } + return data->target; +} + +HTTPResponse::HTTPResponse(NetworkResult res) + : result_(std::move(res)) +{ + DebugCount::increase("lua::api::HTTPResponse"); +} +HTTPResponse::~HTTPResponse() +{ + DebugCount::decrease("lua::api::HTTPResponse"); +} + +int HTTPResponse::data_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->data(L); +} + +int HTTPResponse::data(lua_State *L) +{ + lua::push(L, this->result_.getData().toStdString()); + return 1; +} + +int HTTPResponse::status_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->status(L); +} + +int HTTPResponse::status(lua_State *L) +{ + lua::push(L, this->result_.status()); + return 1; +} + +int HTTPResponse::error_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->error(L); +} + +int HTTPResponse::error(lua_State *L) +{ + lua::push(L, this->result_.formatError()); + return 1; +} + +// NOLINTEND(*vararg) +} // namespace chatterino::lua::api + +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request) +{ + using namespace chatterino::lua::api; + + // Prepare table + SharedPtrUserData::create( + L, std::move(request)); + luaL_getmetatable(L, "c2.HTTPResponse"); + lua_setmetatable(L, -2); + + return lua_gettop(L); +} +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/HTTPResponse.hpp b/src/controllers/plugins/api/HTTPResponse.hpp new file mode 100644 index 00000000000..205aae01e69 --- /dev/null +++ b/src/controllers/plugins/api/HTTPResponse.hpp @@ -0,0 +1,80 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/LuaUtilities.hpp" + +# include +extern "C" { +# include +} + +namespace chatterino { +class PluginController; +} // namespace chatterino + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) + +/** + * @lua@class HTTPResponse + */ +class HTTPResponse : public std::enable_shared_from_this +{ + NetworkResult result_; + +public: + HTTPResponse(NetworkResult res); + HTTPResponse(HTTPResponse &&other) = default; + HTTPResponse &operator=(HTTPResponse &&) = default; + HTTPResponse &operator=(HTTPResponse &) = delete; + HTTPResponse(const HTTPResponse &other) = delete; + ~HTTPResponse(); + +private: + static void createMetatable(lua_State *L); + friend class chatterino::PluginController; + + /** + * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse + * + * If the object given is not a userdatum or the pointer inside that + * userdatum doesn't point to a HTTPResponse, a lua error is thrown. + * + * This function always returns a non-null pointer. + */ + static std::shared_ptr getOrError(lua_State *L, + StackIdx where = -1); + +public: + /** + * Returns the data. This is not guaranteed to be encoded using any + * particular encoding scheme. It's just the bytes the server returned. + * + * @exposed HTTPResponse:data + */ + static int data_wrap(lua_State *L); + int data(lua_State *L); + + /** + * Returns the status code. + * + * @exposed HTTPResponse:status + */ + static int status_wrap(lua_State *L); + int status(lua_State *L); + + /** + * A somewhat human readable description of an error if such happened + * @exposed HTTPResponse:error + */ + + static int error_wrap(lua_State *L); + int error(lua_State *L); +}; + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request); +} // namespace chatterino::lua +#endif From c01bfcfffed874142c6546dbb7a9fc5fe9b7fac2 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Sat, 22 Jun 2024 05:36:29 -0500 Subject: [PATCH 096/105] feat: add `/warn` command (#5474) --- CHANGELOG.md | 1 + mocks/include/mocks/Helix.hpp | 10 + src/CMakeLists.txt | 2 + .../commands/CommandController.cpp | 3 + .../commands/builtin/twitch/Warn.cpp | 199 ++++++++++++++++++ .../commands/builtin/twitch/Warn.hpp | 16 ++ src/providers/twitch/TwitchCommon.hpp | 1 + src/providers/twitch/api/Helix.cpp | 124 +++++++++-- src/providers/twitch/api/Helix.hpp | 26 +++ src/providers/twitch/api/README.md | 44 ++-- 10 files changed, 382 insertions(+), 44 deletions(-) create mode 100644 src/controllers/commands/builtin/twitch/Warn.cpp create mode 100644 src/controllers/commands/builtin/twitch/Warn.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 314884c2647..bb1beeaf686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Minor: Moderators can now see when users are warned. (#5441) - Minor: Added support for Brave & google-chrome-stable browsers. (#5452) - Minor: Added drop indicator line while dragging in tables. (#5256) +- Minor: Added `/warn ` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) - Minor: Introduce HTTP API for plugins. (#5383) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp index 0f0c4110007..14ec3976eb8 100644 --- a/mocks/include/mocks/Helix.hpp +++ b/mocks/include/mocks/Helix.hpp @@ -326,6 +326,16 @@ class Helix : public IHelix (FailureCallback failureCallback)), (override)); // /timeout, /ban + // /warn + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, warnUser, + (QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /warn + // /w // The extra parenthesis around the failure callback is because its type // contains a comma diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a7bcde6703..2546d9a9499 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -107,6 +107,8 @@ set(SOURCE_FILES controllers/commands/builtin/twitch/UpdateChannel.hpp controllers/commands/builtin/twitch/UpdateColor.cpp controllers/commands/builtin/twitch/UpdateColor.hpp + controllers/commands/builtin/twitch/Warn.cpp + controllers/commands/builtin/twitch/Warn.hpp controllers/commands/common/ChannelAction.cpp controllers/commands/common/ChannelAction.hpp controllers/commands/CommandContext.hpp diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index a5554570a87..af96ee9ee90 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -26,6 +26,7 @@ #include "controllers/commands/builtin/twitch/Unban.hpp" #include "controllers/commands/builtin/twitch/UpdateChannel.hpp" #include "controllers/commands/builtin/twitch/UpdateColor.hpp" +#include "controllers/commands/builtin/twitch/Warn.hpp" #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandContext.hpp" #include "controllers/commands/CommandModel.hpp" @@ -439,6 +440,8 @@ void CommandController::initialize(Settings &, const Paths &paths) this->registerCommand("/ban", &commands::sendBan); this->registerCommand("/banid", &commands::sendBanById); + this->registerCommand("/warn", &commands::sendWarn); + for (const auto &cmd : TWITCH_WHISPER_COMMANDS) { this->registerCommand(cmd, &commands::sendWhisper); diff --git a/src/controllers/commands/builtin/twitch/Warn.cpp b/src/controllers/commands/builtin/twitch/Warn.cpp new file mode 100644 index 00000000000..30cc54e6ffe --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Warn.cpp @@ -0,0 +1,199 @@ +#include "controllers/commands/builtin/twitch/Warn.hpp" + +#include "Application.hpp" +#include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +void warnUserByID(const ChannelPtr &channel, const QString &channelID, + const QString &sourceUserID, const QString &targetUserID, + const QString &reason, const QString &displayName) +{ + using Error = HelixWarnUserError; + + getHelix()->warnUser( + channelID, sourceUserID, targetUserID, reason, + [] { + // No response for warns, they're emitted over pubsub instead + }, + [channel, displayName](auto error, auto message) { + QString errorMessage = QString("Failed to warn user - "); + switch (error) + { + case Error::ConflictingOperation: { + errorMessage += "There was a conflicting warn operation on " + "this user. Please try again."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::CannotWarnUser: { + errorMessage += + QString("You cannot warn %1.").arg(displayName); + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); +} + +} // namespace + +namespace chatterino::commands { + +QString sendWarn(const CommandContext &ctx) +{ + const auto command = QStringLiteral("/warn"); + const auto usage = QStringLiteral( + R"(Usage: "/warn [options...] " - Warn a user via their username. Reason is required and will be shown to the target user and other moderators. Options: --channel to override which channel the warn takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, false, true); + + if (!actions.has_value()) + { + if (ctx.channel != nullptr) + { + ctx.channel->addMessage(makeSystemMessage(actions.error())); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } + + return ""; + } + + assert(!actions.value().empty()); + + auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addMessage( + makeSystemMessage("You must be logged in to warn someone!")); + return ""; + } + + for (const auto &action : actions.value()) + { + const auto &reason = action.reason; + if (reason.isEmpty()) + { + ctx.channel->addMessage( + makeSystemMessage("Failed to warn, you must specify a reason")); + break; + } + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Warn Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Warn Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to warn, bad channel name: %1") + .arg(actionChannel.login))); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addMessage(makeSystemMessage( + QString("Failed to warn, bad target name: %1") + .arg(actionTarget.login))); + return; + } + + warnUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addMessage(makeSystemMessage( + QString("Failed to warn, bad username(s): %1") + .arg(userLoginsToFetch.join(", ")))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + warnUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, reason, + action.target.id); + } + } + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Warn.hpp b/src/controllers/commands/builtin/twitch/Warn.hpp new file mode 100644 index 00000000000..42c78f564dc --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Warn.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /warn +QString sendWarn(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/providers/twitch/TwitchCommon.hpp b/src/providers/twitch/TwitchCommon.hpp index 19f538c2a4f..a5c38824500 100644 --- a/src/providers/twitch/TwitchCommon.hpp +++ b/src/providers/twitch/TwitchCommon.hpp @@ -77,6 +77,7 @@ static const QStringList TWITCH_DEFAULT_COMMANDS{ "delete", "announce", "requests", + "warn", }; static const QStringList TWITCH_WHISPER_COMMANDS{"/w", ".w"}; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 460acfdb6ea..5b8c9fbfd09 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -1463,16 +1463,6 @@ void Helix::removeChannelVIP( .execute(); } -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch void Helix::unbanUser( QString broadcasterID, QString moderatorID, QString userID, ResultCallback<> successCallback, @@ -1572,18 +1562,7 @@ void Helix::unbanUser( } }) .execute(); -} // These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch -// These changes are from the helix-command-migration/unban-untimeout branch +} void Helix::startRaid( QString fromBroadcasterID, QString toBroadcasterID, @@ -2266,6 +2245,107 @@ void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID, .execute(); } +// Warn a user +// https://dev.twitch.tv/docs/api/reference#warn-chat-user +void Helix::warnUser( + QString broadcasterID, QString moderatorID, QString userID, QString reason, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixWarnUserError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + QJsonObject payload; + { + QJsonObject data; + data["reason"] = reason; + data["user_id"] = userID; + + payload["data"] = data; + } + + this->makePost("moderation/warnings", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for warning a user was" + << result.formatError() << "but we expected it to be 200"; + } + // we don't care about the response + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("The user specified in the user_id " + "field may not be warned", + Qt::CaseInsensitive)) + { + failureCallback(Error::CannotWarnUser, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 409: { + failureCallback(Error::ConflictingOperation, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error warning user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + // https://dev.twitch.tv/docs/api/reference#send-whisper void Helix::sendWhisper( QString fromUserID, QString toUserID, QString message, diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index c24e44bd3e4..346c9f3c375 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -621,6 +621,18 @@ enum class HelixBanUserError { // /timeout, /ban Forwarded, }; // /timeout, /ban +enum class HelixWarnUserError { // /warn + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + ConflictingOperation, + CannotWarnUser, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /warn + enum class HelixWhisperError { // /w Unknown, UserMissingScope, @@ -1024,6 +1036,13 @@ class IHelix ResultCallback<> successCallback, FailureCallback failureCallback) = 0; + // Warn a user + // https://dev.twitch.tv/docs/api/reference#warn-chat-user + virtual void warnUser( + QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + // Send a whisper // https://dev.twitch.tv/docs/api/reference#send-whisper virtual void sendWhisper( @@ -1346,6 +1365,13 @@ class Helix final : public IHelix ResultCallback<> successCallback, FailureCallback failureCallback) final; + // Warn a user + // https://dev.twitch.tv/docs/api/reference#warn-chat-user + void warnUser( + QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + FailureCallback failureCallback) final; + // Send a whisper // https://dev.twitch.tv/docs/api/reference#send-whisper void sendWhisper( diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 23509c94b8d..18e9d6e4a9d 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -178,13 +178,21 @@ Used in: - `controllers/commands/CommandController.cpp` to send Twitch native shoutout using "/shoutout " +### Warn Chat User + +URL: https://dev.twitch.tv/docs/api/reference/#warn-chat-user + +Used in: + +- `controllers/commands/CommandController.cpp` to warn users via "/warn" command + ## PubSub ### Whispers We listen to the `whispers.` PubSub topic to receive information about incoming whispers to the user -No EventSub alternative available. +The EventSub alternative (`user.whisper.message`) is not yet implemented. ### Chat Moderator Actions @@ -192,25 +200,17 @@ We listen to the `chat_moderator_actions..` PubSub topic to We listen to this topic in every channel the user is a moderator. -No complete EventSub alternative available yet. Some functionality can be pieced together but it would not be zero cost, causing the `max_total_cost` of 10 to cause issues. - -- For showing bans & timeouts: `channel.ban`, but does not work with moderator token??? -- For showing unbans & untimeouts: `channel.unban`, but does not work with moderator token??? -- Clear/delete message: not in eventsub, and IRC doesn't tell us which mod performed the action -- Roomstate (slow(off), followers(off), r9k(off), emoteonly(off), subscribers(off)) => not in eventsub, and IRC doesn't tell us which mod performed the action -- VIP added => not in eventsub, but not critical -- VIP removed => not in eventsub, but not critical -- Moderator added => channel.moderator.add eventsub, but doesn't work with moderator token -- Moderator removed => channel.moderator.remove eventsub, but doesn't work with moderator token -- Raid started => channel.raid eventsub, but cost=1 for moderator token -- Unraid => not in eventsub -- Add permitted term => not in eventsub -- Delete permitted term => not in eventsub -- Add blocked term => not in eventsub -- Delete blocked term => not in eventsub -- Modified automod properties => not in eventsub -- Approve unban request => cannot read moderator message in eventsub -- Deny unban request => not in eventsub +We have not yet migrated to the EventSub equivalent topics: + +- For showing bans & timeouts => `channel.moderate` +- For showing unbans & untimeouts => `channel.moderate` +- Clear/delete message => `channel.moderate` +- Roomstate (slow(off), followers(off), r9k(off), emoteonly(off), subscribers(off)) => `channel.moderate` +- VIP/Moderator added/removed => `channel.moderate` +- Raid started/cancelled => `channel.moderate` +- Add/delete permitted/blocked term => `channel.moderate` (or `automod.terms.update`) +- Modified automod properties => `automod.settings.update` +- Approve/deny unban request => `channel.moderate` (or `channel.unban_request.resolve`) ### AutoMod Queue @@ -218,7 +218,7 @@ We listen to the `automod-queue..` PubSub topic to rec We listen to this topic in every channel the user is a moderator. -No EventSub alternative available yet. +The EventSub alternative (`automod.message.hold` and `automod.message.update`) is not yet implemented. ### Channel Point Rewards @@ -230,4 +230,4 @@ The EventSub alternative requires broadcaster auth, which is not a feasible alte We want to listen to the `low-trust-users` PubSub topic to receive information about messages from users who are marked as low-trust. -There is no EventSub alternative available yet. +The EventSub alternative (`channel.suspicious_user.message` and `channel.suspicious_user.update`) is not yet implemented. From 2ef3306d1d29fd24705b752fccf646ee7b7bf5b6 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Sat, 22 Jun 2024 06:03:49 -0500 Subject: [PATCH 097/105] feat: notate power-up automatic reward redemptions (#5471) --- CHANGELOG.md | 1 + src/providers/twitch/ChannelPointReward.cpp | 48 +++++++++++++++++++ src/providers/twitch/ChannelPointReward.hpp | 3 ++ src/providers/twitch/IrcMessageHandler.cpp | 29 +++++++---- src/providers/twitch/PubSubManager.cpp | 2 + src/providers/twitch/TwitchMessageBuilder.cpp | 15 ++++++ .../twitch/pubsubmessages/ChannelPoints.hpp | 4 ++ 7 files changed, 92 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1beeaf686..e77706f91d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Minor: Moderators can now see when users are warned. (#5441) - Minor: Added support for Brave & google-chrome-stable browsers. (#5452) - Minor: Added drop indicator line while dragging in tables. (#5256) +- Minor: Add channel points indication for new bits power-up redemptions. (#5471) - Minor: Added `/warn ` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) - Minor: Introduce HTTP API for plugins. (#5383) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) diff --git a/src/providers/twitch/ChannelPointReward.cpp b/src/providers/twitch/ChannelPointReward.cpp index 62d93e3f0c2..658d498ff68 100644 --- a/src/providers/twitch/ChannelPointReward.cpp +++ b/src/providers/twitch/ChannelPointReward.cpp @@ -14,6 +14,47 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) this->title = reward.value("title").toString(); this->cost = reward.value("cost").toInt(); this->isUserInputRequired = reward.value("is_user_input_required").toBool(); + this->isBits = reward.value("pricing_type").toString() == "BITS"; + + // accommodate idiosyncrasies of automatic reward redemptions + const auto rewardType = reward.value("reward_type").toString(); + if (rewardType == "SEND_ANIMATED_MESSAGE") + { + this->id = "animated-message"; + this->isUserInputRequired = true; + this->title = "Message Effects"; + } + else if (rewardType == "SEND_GIGANTIFIED_EMOTE") + { + this->id = "gigantified-emote-message"; + this->isUserInputRequired = true; + this->title = "Gigantify an Emote"; + } + else if (rewardType == "CELEBRATION") + { + this->id = rewardType; + this->title = "On-Screen Celebration"; + const auto metadata = + redemption.value("redemption_metadata").toObject(); + const auto emote = metadata.value("celebration_emote_metadata") + .toObject() + .value("emote") + .toObject(); + this->emoteId = emote.value("id").toString(); + this->emoteName = emote.value("token").toString(); + } + + // use bits cost when channel points were not used + if (cost == 0) + { + this->cost = reward.value("bits_cost").toInt(); + } + + // workaround twitch bug where bits_cost is always 0 in practice + if (cost == 0) + { + this->cost = reward.value("default_bits_cost").toInt(); + } // We don't need to store user information for rewards with user input // because we will get the user info from a corresponding IRC message @@ -27,6 +68,13 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) } auto imageValue = reward.value("image"); + + // automatic reward redemptions have specialized default images + if (imageValue.isNull() && this->isBits) + { + imageValue = reward.value("default_image"); + } + // From Twitch docs // The size is only an estimation, the actual size might vary. constexpr QSize baseSize(28, 28); diff --git a/src/providers/twitch/ChannelPointReward.hpp b/src/providers/twitch/ChannelPointReward.hpp index f9f9b6316e6..d4f428e92f5 100644 --- a/src/providers/twitch/ChannelPointReward.hpp +++ b/src/providers/twitch/ChannelPointReward.hpp @@ -19,6 +19,9 @@ struct ChannelPointReward { int cost; ImageSet image; bool isUserInputRequired = false; + bool isBits = false; + QString emoteId; // currently only for celebrations + QString emoteName; // currently only for celebrations struct { QString id; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index d0600b8fa53..4657438ad2b 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1338,21 +1338,30 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, auto *channel = dynamic_cast(chan.get()); const auto &tags = message->tags(); + QString rewardId; if (const auto it = tags.find("custom-reward-id"); it != tags.end()) { - const auto rewardId = it.value().toString(); - if (!rewardId.isEmpty() && - !channel->isChannelPointRewardKnown(rewardId)) + rewardId = it.value().toString(); + } + else if (const auto typeIt = tags.find("msg-id"); typeIt != tags.end()) + { + // slight hack to treat bits power-ups as channel point redemptions + const auto msgId = typeIt.value().toString(); + if (msgId == "animated-message" || msgId == "gigantified-emote-message") { - // Need to wait for pubsub reward notification - qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " - "callback since reward is not known:" - << rewardId; - channel->addQueuedRedemption(rewardId, originalContent, message); - return; + rewardId = msgId; } - args.channelPointRewardId = rewardId; } + if (!rewardId.isEmpty() && !channel->isChannelPointRewardKnown(rewardId)) + { + // Need to wait for pubsub reward notification + qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " + "callback since reward is not known:" + << rewardId; + channel->addQueuedRedemption(rewardId, originalContent, message); + return; + } + args.channelPointRewardId = rewardId; QString content = originalContent; int messageOffset = stripLeadingReplyMention(tags, content); diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index cefde91696a..447909812b9 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1181,6 +1181,8 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) switch (innerMessage.type) { + case PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: { auto redemption = innerMessage.data.value("redemption").toObject(); diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index f69264cb3b1..7fa77e4947a 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1594,6 +1594,15 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( } builder->emplace(redeemed, MessageElementFlag::ChannelPointReward); + if (reward.id == "CELEBRATION") + { + const auto emotePtr = + getIApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote( + EmoteId{reward.emoteId}, EmoteName{reward.emoteName}); + builder->emplace(emotePtr, + MessageElementFlag::ChannelPointReward, + MessageColor::Text); + } builder->emplace( reward.title, MessageElementFlag::ChannelPointReward, MessageColor::Text, FontStyle::ChatMediumBold); @@ -1602,6 +1611,12 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( builder->emplace( QString::number(reward.cost), MessageElementFlag::ChannelPointReward, MessageColor::Text, FontStyle::ChatMediumBold); + if (reward.isBits) + { + builder->emplace( + "bits", MessageElementFlag::ChannelPointReward, MessageColor::Text, + FontStyle::ChatMediumBold); + } if (reward.isUserInputRequired) { builder->emplace( diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp index be8d1bd68af..fe2254a7a67 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp @@ -8,6 +8,7 @@ namespace chatterino { struct PubSubCommunityPointsChannelV1Message { enum class Type { + AutomaticRewardRedeemed, RewardRedeemed, INVALID, @@ -30,6 +31,9 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< { switch (value) { + case chatterino::PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: + return "automatic-reward-redeemed"; case chatterino::PubSubCommunityPointsChannelV1Message::Type:: RewardRedeemed: return "reward-redeemed"; From 0b54b0b8f5553681e233f9526c22b6060bb0e48e Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 22 Jun 2024 13:38:12 +0200 Subject: [PATCH 098/105] refactor: cleanup browser extension (#5465) --- CHANGELOG.md | 1 + src/BrowserExtension.cpp | 114 +++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77706f91d5..03a676036c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) - Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) - Dev: Refactor/unsingletonize `UserDataController`. (#5459) +- Dev: Cleanup `BrowserExtension`. (#5465) ## 2.5.1 diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index 3ac8dc2dded..8511cbcb078 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -2,13 +2,6 @@ #include "singletons/NativeMessaging.hpp" -#include -#include -#include -#include - -#include -#include #include #include #include @@ -16,69 +9,88 @@ #ifdef Q_OS_WIN # include # include -# include -#endif -namespace chatterino { +# include + +#endif namespace { - void initFileMode() - { + +using namespace chatterino; + +void initFileMode() +{ #ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); #endif - } - - void runLoop() - { - auto received_message = std::make_shared(true); +} - auto thread = std::thread([=]() { - while (true) - { - using namespace std::chrono_literals; - if (!received_message->exchange(false)) - { - _Exit(1); - } - std::this_thread::sleep_for(5s); - } - }); +// TODO(Qt6): Use QUtf8String +void sendToBrowser(QLatin1String str) +{ + auto len = static_cast(str.size()); + std::cout.write(reinterpret_cast(&len), sizeof(len)); + std::cout.write(str.data(), str.size()); + std::cout.flush(); +} - while (true) - { - char size_c[4]; - std::cin.read(size_c, 4); +QByteArray receiveFromBrowser() +{ + uint32_t size = 0; + std::cin.read(reinterpret_cast(&size), sizeof(size)); - if (std::cin.eof()) - { - break; - } + if (std::cin.eof()) + { + return {}; + } - auto size = *reinterpret_cast(size_c); + QByteArray buffer{static_cast(size), + Qt::Uninitialized}; + std::cin.read(buffer.data(), size); - std::unique_ptr buffer(new char[size + 1]); - std::cin.read(buffer.get(), size); - *(buffer.get() + size) = '\0'; + return buffer; +} - auto data = QByteArray::fromRawData(buffer.get(), - static_cast(size)); - auto doc = QJsonDocument(); +void runLoop() +{ + auto receivedMessage = std::make_shared(true); - if (doc.object().value("type") == "nm_pong") + auto thread = std::thread([=]() { + while (true) + { + using namespace std::chrono_literals; + if (!receivedMessage->exchange(false)) { - received_message->store(true); + sendToBrowser(QLatin1String{ + R"({"type":"status","status":"exiting-host","reason":"no message was received in 10s"})"}); + _Exit(1); } + std::this_thread::sleep_for(10s); + } + }); - received_message->store(true); - - nm::client::sendMessage(data); + while (true) + { + auto buffer = receiveFromBrowser(); + if (buffer.isNull()) + { + break; } - _Exit(0); + + receivedMessage->store(true); + + nm::client::sendMessage(buffer); } + + sendToBrowser(QLatin1String{ + R"({"type":"status","status":"exiting-host","reason":"received EOF"})"}); + _Exit(0); +} } // namespace +namespace chatterino { + void runBrowserExtensionHost() { initFileMode(); From 83b4edef4eaa8a66e01425004410ebd3089cc390 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:06:00 +0000 Subject: [PATCH 099/105] chore(deps): bump lib/expected-lite from `3634b0a` to `182165b` (#5469) --- lib/expected-lite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/expected-lite b/lib/expected-lite index 3634b0a6d8d..182165b584d 160000 --- a/lib/expected-lite +++ b/lib/expected-lite @@ -1 +1 @@ -Subproject commit 3634b0a6d8dffcffad4d1355253d79290c0c754c +Subproject commit 182165b584dad130afaf4bcd25b8629799baea38 From c1e4d17e468ce8d27127ce5ce6a76881539fdd5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:49:51 +0000 Subject: [PATCH 100/105] chore(deps): bump lib/settings from `80b8cb6` to `f3e9161` (#5468) --- lib/settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/settings b/lib/settings index 80b8cb605a8..f3e9161ee61 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 80b8cb605a82134c2ce5abf56715c3443413e947 +Subproject commit f3e9161ee61b47da71c7858e24efee9b0053357f From 5a5590fc8c548a03c71f74df47073328bb85054c Mon Sep 17 00:00:00 2001 From: JakeRYW Date: Sun, 23 Jun 2024 06:22:06 -0400 Subject: [PATCH 101/105] Adding JakeRYW to contributor list (#5481) --- resources/avatars/jakeryw.png | Bin 0 -> 6603 bytes resources/contributors.txt | 1 + 2 files changed, 1 insertion(+) create mode 100644 resources/avatars/jakeryw.png diff --git a/resources/avatars/jakeryw.png b/resources/avatars/jakeryw.png new file mode 100644 index 0000000000000000000000000000000000000000..fd256af642d3df7254342baf12488f6c53d52bdf GIT binary patch literal 6603 zcmV;+88qgJP)C0008wP)t-s|Ns91 z1OSzUbl1J8v7U(W+{NwK!SU6=+Py6euf>dmt0%(3Xp zu;t0F;>D`t#iZfGqTs@z-M^pT!Jyi_oY}jY+O?P1vX0icnA5I`-ME_BvyaxWjn}i4 z+_sX_wUx}KgUF+R&#H&vyOz?kjS>wDH7_a{6%?C`dm$VaI5RDaduwuHQ*B^YC?g&$ zCn85cHN&u(KRGkdwV;M}W-%-$Bp({pxSq(ZnN3DJwxW!=pNpoGetc|QyQq?AT2YXH za8XJ>z^0O(i*{5-I2siXO+PVHMm*lbuC1GeTTMh97#GmAnqyQ&%B+mau9RU@N_J&X zer;H%jBROHL|RKd(5#L@Ixx7KeAu~?+`X4GEFyJgT|P1?z@vm|T1y5A1-6@l!JT=G zb6ca5fqiCEI4mTJcw?lAaKWXFd05}bl*lZ1N3pLUFUW=1z9ZB8$na6offK2$yNN(Y^}Y zcmx6h384rixNzZNIB-F%w1QJkmSZqpduOupqvR6DLlbIC0`uH2%Xobl-5__RV&+AXe+my+ycjk7ie>H~=t)>_XSm zZDYqh=od35gdzk3fxxvh+F9ElH3E06UuIlM0i@?3I0prQPNNTD>ld*PccP050AmUm5)(@gWQ30V8$w`l>BQXFa$g1kMqb3OYOnG-y3&H6Y zAQ&(N=G$*5K^OuG(66d$b-Vx#&q(edy$mS;06`$J?|m6-OO#!EbY0)^*U| z@YepzK^(mpK#8Px-G|NaINUgQ>6%JEJeO5@> zquZkjDxT~x7Glf=fEdRCm^i!J|NQjVmz%~Y9fs=jw6rDRtV`+N9jafN9#tOL@4O1) z{=*TF@o)kdZNm{*&6lg~dcECl8dZ8l`kc(y_PV5Z6G}KeFu-DlgKl@w=}9GtqEITV z0?B~^-6()DMD2X@t0`&!#b};hrTzn0=YJkkAJBqzWo4JXJq3_ELjB>vfeS%=%g$iX^An(oLWcc+4@3|(M>pH<cN&ADOdG_EzJ2`7UZa!`&Nh>*f`ZK+@#&3}rGW^rNl?S(Ro%ea-EdsHWhs9zsAn^UZ-*>*dDC3ivmM7NM;PcALKGoZGW)J|b3jx>b$3EXdKk^^RH<0G)AFZBN5w5bUBChW0x@nqj zI1;V0-RZXNRl2uSRn8!z$=;4LXXB|36tKPzu4g=8?eqfFcSA4VpdKSYCtHA=WG>qJ zP&8^!w_L}o&|OovOhXGBhH5!Z*z@e^x^drheJddyz30z#o=$c4rcPex8kRr?vIOAC zLXSlNh=laNQRO(^G*4BvfHhSOyNao*x}i>WOiHIKs-~EhI{hv}&ZV$w=>_bugiQ|* ze^v+tay%7h7$DqutiT2`JoX-Sys+hXsE9b>8%9VDLp_%~;;ctFxTGTqS(Q$>WpNB7ii`HUot4)rn! ztOWXHH}mj4d+f?;&ou8dO;x9|q}u^W5+z9x6l?V}Xa2VT@JwB^V1-$sE3yi*ZJi1sQ4&HjSWpzx zkd#td3O9g(|ABU7zzIU;$d;|>=Jc?xJNPz&^}t=9f`CkxFa4KgI=4)}*U(|5B8#Px zrI@-XDT*kHvLL9`pq8mns>q;V2`bUYypthv7HTR(t7(d4=$36qU8nq$5pe5ac24g8 z@-(xWS}L^5(7{)hA1kC&?i!)@|@_6vW$NF z+Y&{%33!AAC=X$Seo+<$@B&ag2xYjyXCU52$XY6uvQMZMFb$n$aI`$?d3Rh30D;Vs zb`x@|DY#0NVN6wBiQ<=KD+t#y5ZN>vMv~qoD_KHJWXamziG*OfI?JBw{$1sd9H3*<+X%N(*Gw53krT8 z>?f}T#UT696})UC2o}zwi21!Vl>heP5M+#kMR^MsE)0y>&k6#MN(lgtceC@EsNgb% zPc#?wOW3rq3^%QKlmb<5eNzM`ZsP^ zmnV#~UgU0&L6@)w3*1C9^*)BdHKM}eLSXz;-9jALu2pW8vq83>kn#GDl^_60eh1#f zP;k}Uyt2^h1@jgx&Z8kDL+!~Q)z=?tIQ%?$M+ixqHf-#FrFgsp_=9gNauK?a*^kIz zB?TfI+Em-Y!vY6i!sp`vyj}zb3i4URhsyYGVw*K%+iiGRPkvlqU*Fu^(EJlJ#V39B zrsKa+4E=I{m#mLvxE>YPvBpaDCQE~!#n`>C)V#`|1#K6#+XRC(|#n zdL)?!MkzCDA_?4bZ~v75SW=f16&970%qBAtPyoP<@#Wq0j~g1A!2iL+%`JbAt^^UT zg#ZF?@VyU11OW5{%ua|ws%nN2WIQi{SypU^dgl+V9sNI#l1(3;-bDXlrIRXvJKZGdSyfAr}i{ zd%mHun}*<;P_CPxz|?KWig=sH1LzOFR00IAVHzrv%dQ|{9r@=&^^Ner;pXO}M~@tR zcFRoBpT;KB&H?0bKztNb*RUB>(=}DoOqIHx<2*PXz)pd+z~Ed$-V(X;EP{PCjg5y6 z9&Bzr+)4*;0)Q0`;cK+pvnni`v)`OeXnqpi=jwmz3R_9|Z=2trox_*yuw z9&iGXh1euyp<$~E1q9vx+lj5xbb2KxP@?%Z{4*Lx&rin!cAIFSZ>0;nSH9 zpZ^JFk=bwO0|V2L)iREDpNedVF#ZKukz`fTAcpNl)bdHaXwyrqY`%?oNGJp&5GsLI3IMzv=$38mB@^e7IfX@nQ1lj>f&d_)U|Fy%G=&_`eD`2u^Wh(m zOMN{fxQ5A2!KTi4#A31b_OAG;k$H=Ap_dzubCDo$p%Tkl*l!^U1Ey(N(c32hc(Vj( z%YI78JAcT){L#yu$1?{To_UdEdT@Dg;Cw0t0KGW<8N=0EAm|=h$*dHzW^-; zHs%CsI5dE2U|ELcIe#_@z>H$FtTN#knF+6P53C!>#A1D9%L^mqQcq{911G<6x)2yr z9gF}>{_Wi-7lGnj%w?VLKiH1q1t}m%R)BiQKMNsH={S!}9?JVEaE92LWKLqczI-$keZFFa#O&S z9U$igi9)}Ko#Cp6Gc}ApFx4sl*63B%MZ1@?uVAQO50l`8og~#2d;g=s!DL6QZRk=a zP12dmCr+hOsU%stV8_0>bJoGV64#X5;U$X$Edve!0!+o47uKl})-~OZn3WL_Fd0Bi zF-z*aVNy#O0HJ2&1FSA*ZzF>rjga)bZ|+;as}KJ?Qm8 zWRFEYl2K1X8k#l+a_p{lf`l;YEv!lxp%}~T$2M$xV9jzWZ6|3`Q@4vViDG+b4~!HYsFN z?V>3GD)X@l0I0Nh$%5?^%4O&m7!5*Bvcc##N+19lH~`-^HEK$h>*!Ybv^%eLdgSMv ztQvCccW@v401fDSek-9sfFmALXe&IkISc^^_=984Hb*f#u>e6S!zwE>GSG7!$9aQX z3&8wc;7mjwtQAA-_o0tL;+EruY2~B{41?@9S!^)Zr#|a?2|Efv5+_7b*t)FJwX1J& z9h874hPnQ$BB)!wr&wBnKC>9qmIzF*2o5MefGar)zqgJ(1TcXus~#fyf#BgIk-uND zGM^c*N2@)j(mSx3e8MTZ;T6TjmGVYR#(bgA6^Q~E;73>3oM=8p+ z!v*Ft@-7WjRhxE|hZBUg*VOMXkM`~=rmNCqcs2iimUI?zG$+NYw{P-Epb~tBLy&-w z=alolBN%>(K&HUrUz9slR;5>c;9I*h3lx@rZgZjYhgj^aLM2(ui+J{wq>y$MsfKe9Z5UApXNfu;-k!J2e*W2X5SQS8D${as21YLy8UgLYccmc`ub>S3T`w+?e5&;l+`xt?Y zyvq_s01AWz{9u?=XZ3IYb>4VMAep-5#hA=yE+UswXht~a$!-E5%j#Ay-!!L|jC6LR z4|gTnKT4B%Z|>+PnG-|DFO8C0S(@;yQ4lyPRjF=Sj|^Xjb}l~UTYvs*W)*ZZ1h|^s z0SLZa{8wDdY;R8_6ES`%X=E^&N}W45K<@VIqyQ#VAkkgT4U{kCS_zlyyOCprQIYho z;kPpwr$0QN$z(og>t_1n$z&`MPbAP86Uk28%hA?0x{Qjt4)qXO1r*IvESB>(a_4Fq z2sjD+Rua5m20I;(V*`w2vJ)^;v1BaC?xo4hR3tX~I{~UWnoQXOY`1Ut4L}&}<2TXx z7;up2jweocB@J9%m;|h zcsvoqGY*sEv95R$l1L>mAtw5I&YjC-kd(TrXvS1ex4^=^Z=|#OZv;eokn8V?f%yaj zlQ<0=$wWJ}V*?4PWU>!;GF>K5vD3m8hH8{!=yTj(4&Rvm+;!a8X&_*c=L?RRGh!(q zU=K`axNBr!h}2O{*O7V#`1cO{dzSwjhmh%k$~YrGmgq{vyOZF6Y4$lt^&kS-y-Z}J z*t9h>9PuopV$c2L^SrBZ@N}Z9J=WFPh5V17P9hALBv>9KdN8G5=mUVuq%LAwy6(7^ z@z-0(=Xzf!NMN)lD9dbpzED_hCueq$@Sm^uyH z0-P z6da#I^j{c&^``~~uni1Cxi4Sn4fdIfgKd2fP;ddxXW<6~gYTgs|MC9EVD!@Ep&{JE zh|nJzdhhaYN7H|_|MVcAEdQ7P=5q62k8a9MxhXg0ru@H^{{@CdwDm?`AfEsL002ov JPDHLkV1i4ZG6Mhr literal 0 HcmV?d00001 diff --git a/resources/contributors.txt b/resources/contributors.txt index 8fdda0e286b..6267887e35f 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -78,6 +78,7 @@ fraxx | https://github.com/fraxxio | :/avatars/fraxx.png KleberPF | https://github.com/KleberPF | nealxm | https://github.com/nealxm | :/avatars/nealxm.png Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png +JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png # If you are a contributor add yourself above this line From bd8fcd65ec5581d0db71874c77014a8705a04b54 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 23 Jun 2024 12:30:33 +0200 Subject: [PATCH 102/105] chore: change pajlada's profile picture to v2 (#5482) --- resources/avatars/pajlada.png | Bin 2255 -> 306 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/avatars/pajlada.png b/resources/avatars/pajlada.png index c596bfb8edf39fddafd1350d71cc32521b59b366..e8ff6160d64e1900a61d0fb84fb576bdf25fd182 100644 GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvK8A$4HYMBD0I0Jk_T>t<7&&&)GTzi4EaktaqI21gPeyAcv>H}_wOv{?7gOGO}H+uKBMT+9)f;E~2sAu&NGg69L< z31Nql`X~nOfQF+D1`g~y?5g&jOn$+_q0U@cwSbjFV1cyCBuj^e{|8O4b;&D)IIt^B zl#$>l4PWlZutDm?a!qT!*Nk<{1`MGWxD~c;lDbeNEF!!>)Oo>Oc~>3>>2v%g73(6H zw@Gv4?3$=3*AP?Ro~ttuLwLK-gL?}80cRHPgg&ebxsLQ0ADq3 A8~^|S literal 2255 zcmV;=2r&1FP)=2O~+>;tYH`qi*-2!sGb&mPiIh_1poBI zMKxp1SToj)bwHu-`&*xB814^>b+jIajo&HV!Zz5|dT>$ASToj)HDg6JW7WaJa=LyA z){~4xJQO=)yB<%wQ-}DY!fS?3k(m7E>~yLk$hg=__gWQA;~RHo{4`trLzh4Mii`xB``v!W6>cC z%WP65w`wK`*2ZEMmT8@@eqzWe_r!n&89Wb`z=^Km=W4BBsYA;i7E?$N_7X~DU?LWs zDh4?CalkS!!SBq5FbqM*jP!u8Tj_9DhM`2`<&BA02z(m`EU~JZTY|?7x9nKR%@54O zVym(B)v*vN`Ej{i(uvT9TS}qzvBV2DfyETJFk+vAg=AXc>4VWkg4 zQL#|T@&+tR4`8V_jzl7H(sUUZi9-w(M9Ul*2+HtOL8nHnH;Ek~5))6>qiA(TG8@9W1u* zmIy&CISt0Kq)Woj9b40~{Qc^t1{RENU38rjHIpFDT|j6E3wF1-OQB-PoCka?|LTK7 zBW|@HTlKKcmhW1)Hn8Bvs_GV;SvnRgxkcSUB)J76maap^78=5WL*s6xnMa2>7Ud`w zz+ySKnD3TR+)E=e+-5j*Z0Uy6j>Ts37WWZbI7ouv#C*yWEY!Xd(MCF@hf*C2X3H4B zqH`$uH@Bv#)Cp3(nDil`j?RU0)gt z-K!L=i;mv6s$!k2UbTr}$?^>Ul{z_COnuDpvPlfftDW+rX0uxiU7?tSLV9jr?Aef@v0J!`T4lF zdX0AO5sNC8d{@Jw8B0pZz-q1$GqBhuR-k++?h&;4NLkyjP3S=J}B07%UfIyXHXq1`@&At z!?KIDu{hN$R=|R4NA#nX8}WSzdR-U`a{5#(qq0F?dAA?{k$ro17rbB${jGK*!l|Nn zcv&n1pQt#N#>c&4bzNt*$&MxOjJuVM41YKzb5Q1s{r{i=%Rlg3vq>QI15pDD84XH* z=fkRQAz+V=i5M1wSY23GyWR6Zjf`4IOUU23tyW=eb{}S__)#Q5Jih;c@L7?R0Q+V( zRInthlCcbVR8$8GK7|%vl>cKLEL~932&-f1Lj6Wu^rHAKH6o_O*5dzCE{8YW309&8 zk(9u3b%V0V7=rbtKOD#2xt4l^7^FIHSOwAJ9)j?8EXW@oR#*yFS+_pK5^~nYdhA6% z-lF}i0k@t8(ZyS|ZyLZFAxKRlEVhZ&s9OfCA*KwhoBk|{#qgV+G^{?NvrSI;Z+tA} zy;$~r=kn!edF+rifF+`XiUo)2Sn!rRdsy(sew0m5V@kXXdasrsmV)HPWlTRcA{o<9 z$Fl2N>RofjGGNICzz?ywH!P@bF&j$CWTi%=E1;wJ5EUFD& z9t+N(!dUj5;aLKUB;Y7m6HP+oO}v*Auh3eNLAVEd-=izx~`9gf_7nb*o)0 zR>wkCRmhGo$oQW+|KquZDkjN<>PEr3Nn1C(?|%EW{mF&3u%vJ*Z{btLqFDO&Z;)zZ z2T=*hUM5uIiqGOl!C-2~qjf^CKlHDn{>DAT5m0>2=b>0MqZ*c8Clr>NSRBW5q Date: Sun, 23 Jun 2024 13:32:12 +0200 Subject: [PATCH 103/105] fix(windows): wait for Qt before getting bounds (#5478) --- CHANGELOG.md | 1 + src/widgets/BaseWindow.cpp | 11 ++++++++--- src/widgets/BaseWindow.hpp | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a676036c5..35717f6af82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Bugfix: Fixed message history occasionally not loading after a sleep. (#5457) - Bugfix: Fixed a crash when tab completing while having an invalid plugin loaded. (#5401) +- Bugfix: Fixed windows on Windows not saving correctly when snapping them to the edges. (#5478) - Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) - Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) - Dev: Unsingletonize `ISoundController`. (#5462) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 6d880067ec9..6d636b9d6b5 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -240,7 +240,7 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) #ifdef USEWINSDK this->useNextBounds_.setSingleShot(true); QObject::connect(&this->useNextBounds_, &QTimer::timeout, this, [this]() { - this->currentBounds_ = this->nextBounds_; + this->currentBounds_ = this->geometry(); }); #endif @@ -1137,7 +1137,11 @@ bool BaseWindow::handleSIZE(MSG *msg) if (this->isNotMinimizedOrMaximized_) { - this->currentBounds_ = this->geometry(); + // Wait for WM_SIZE to be processed by Qt and update the current + // bounds afterwards. + postToThread([this] { + this->currentBounds_ = this->geometry(); + }); } this->useNextBounds_.stop(); @@ -1166,7 +1170,8 @@ bool BaseWindow::handleMOVE(MSG *msg) #ifdef USEWINSDK if (this->isNotMinimizedOrMaximized_) { - this->nextBounds_ = this->geometry(); + // Wait for WM_SIZE (in case the window was maximized, we don't want to + // save the bounds but keep the old ones) this->useNextBounds_.start(10); } #endif diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 946c9e24543..2f21686457a 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -188,7 +188,6 @@ class BaseWindow : public BaseWidget QRect initalBounds_; QRect currentBounds_; - QRect nextBounds_; QTimer useNextBounds_; bool isNotMinimizedOrMaximized_{}; bool lastEventWasNcMouseMove_ = false; From 189be8c68f27b5aeec3b864574421279667811c9 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 23 Jun 2024 14:26:59 +0200 Subject: [PATCH 104/105] chore: remove old Qt 5.12 code, update docs and FreeBSD runner (#5396) Co-authored-by: Rasmus Karlsson --- .cirrus.yml | 5 +++-- BUILDING_ON_FREEBSD.md | 12 ++++++------ BUILDING_ON_LINUX.md | 8 ++++---- BUILDING_ON_MAC.md | 2 +- BUILDING_ON_WINDOWS.md | 10 +++++----- BUILDING_ON_WINDOWS_WITH_VCPKG.md | 4 ++-- CHANGELOG.md | 1 + src/common/LinkParser.cpp | 19 ------------------- src/common/LinkParser.hpp | 11 +++-------- .../commands/CommandController.cpp | 1 - src/messages/MessageBuilder.cpp | 1 - src/messages/SharedMessageBuilder.cpp | 1 - src/messages/search/AuthorPredicate.cpp | 1 - src/messages/search/BadgePredicate.cpp | 1 - src/messages/search/ChannelPredicate.cpp | 1 - src/messages/search/LinkPredicate.cpp | 1 - src/messages/search/MessageFlagsPredicate.cpp | 2 -- src/messages/search/SubtierPredicate.cpp | 1 - src/providers/twitch/TwitchMessageBuilder.cpp | 12 ------------ src/util/DebugCount.cpp | 4 ---- src/util/Helpers.cpp | 12 ++++-------- src/util/Helpers.hpp | 17 ++++------------- src/util/QStringHash.hpp | 16 ---------------- src/util/Qt.hpp | 9 --------- src/util/XDGDirectory.cpp | 1 - src/util/XDGHelper.cpp | 1 - src/widgets/helper/ResizingTextEdit.cpp | 4 ---- src/widgets/settingspages/CommandPage.cpp | 1 - src/widgets/splits/SplitInput.cpp | 4 ---- tests/src/Helpers.cpp | 10 ++-------- 30 files changed, 35 insertions(+), 138 deletions(-) delete mode 100644 src/util/Qt.hpp diff --git a/.cirrus.yml b/.cirrus.yml index 7d957444b18..d4ac409cd92 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,9 +1,9 @@ freebsd_instance: - image: freebsd-13-1-release-amd64 + image_family: freebsd-14-0 task: install_script: - - pkg install -y boost-libs git qt5-buildtools qt5-concurrent qt5-core qt5-multimedia qt5-svg qtkeychain-qt5 qt5-qmake cmake qt5-linguist + - pkg install -y boost-libs git qt6-base qt6-svg qt6-5compat qt6-imageformats qtkeychain-qt6 cmake script: | git submodule init git submodule update @@ -20,6 +20,7 @@ task: -DUSE_SYSTEM_QTKEYCHAIN="ON" \ -DCMAKE_BUILD_TYPE="release" \ -DCMAKE_EXPORT_COMPILE_COMMANDS="ON" \ + -DBUILD_WITH_QT6="ON" \ .. cat compile_commands.json make -j $(getconf _NPROCESSORS_ONLN) diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md index 26e751c939e..38b603a2c5c 100644 --- a/BUILDING_ON_FREEBSD.md +++ b/BUILDING_ON_FREEBSD.md @@ -1,15 +1,15 @@ # FreeBSD -Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.12 or newer**. +For all dependencies below we use Qt 6. Our minimum supported version is Qt 5.15.2, but you are on your own. -## FreeBSD 12.1-RELEASE +## FreeBSD 14.0-RELEASE -Note: This is known to work on FreeBSD 12.1-RELEASE amd64. Chances are +Note: This is known to work on FreeBSD 14.0-RELEASE amd64. Chances are high that this also works on older FreeBSD releases, architectures and -FreeBSD 13.0-CURRENT. +FreeBSD 15.0-SNAP. 1. Install build dependencies from package sources (or build from the - ports tree): `# pkg install qt5-core qt5-multimedia qt5-svg qt5-buildtools gstreamer-plugins-good boost-libs rapidjson cmake` + ports tree): `# pkg install boost-libs git qt6-base qt6-svg qt6-5compat qt6-imageformats qtkeychain-qt6 cmake` 1. In the project directory, create a build directory and enter it ```sh mkdir build @@ -17,7 +17,7 @@ FreeBSD 13.0-CURRENT. ``` 1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh - cmake .. + cmake -DBUILD_WITH_QT6=ON .. ``` 1. Build the project ```sh diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 3aa2df4a83f..51317ecd1ff 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -1,6 +1,6 @@ # Linux -For all dependencies below we use Qt6. Our minimum supported version is Qt5.15, but you are on your own. +For all dependencies below we use Qt 6. Our minimum supported version is Qt 5.15.2, but you are on your own. ## Install dependencies @@ -8,11 +8,11 @@ For all dependencies below we use Qt6. Our minimum supported version is Qt5.15, Building on Ubuntu requires Docker. -Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-20.04 as your base if you're on Ubuntu 20.04. +Use as your base if you're on Ubuntu 20.04. -Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-22.04 if you're on Ubuntu 22.04. +Use if you're on Ubuntu 22.04. -The built binary should be exportable from the final image & able to run on your system assuming you perform a static build. See our [build.yml github workflow file](.github/workflows/build.yml) for the cmake line used for Ubuntu builds. +The built binary should be exportable from the final image & able to run on your system assuming you perform a static build. See our [build.yml GitHub workflow file](.github/workflows/build.yml) for the CMake line used for Ubuntu builds. ### Debian 12 (bookworm) or later diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index 1c8fc38a1cf..d1fc018b90b 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -1,6 +1,6 @@ # Building on macOS -Chatterino2 is built in CI on Intel on macOS 12. +Chatterino2 is built in CI on Intel on macOS 13. Local dev machines for testing are available on Apple Silicon on macOS 13. ## Installing dependencies diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index 42d71cc5133..2449a329d00 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -24,7 +24,7 @@ Notes: Notes: -- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.12 or later**. +- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.15.2 or later**. #### Components @@ -33,7 +33,7 @@ When prompted which components to install, do the following: 1. Unfold the tree element that says "Qt" 2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 6.5.3`) 3. Under this version, select the following entries: - - `MSVC 2019 64-bit` (or alternative version if you are using that) + - `MSVC 2019 64-bit` (or `MSVC 2022 64-bit` from Qt 6.8 onwards) - `Qt 5 Compatibility Module` - `Additional Libraries` > `Qt Image Formats` 4. Under the "Tools" tree element (at the bottom), ensure that `Qt Creator X.X.X` and `Debugging Tools for Windows` are selected. (they should be checked by default) @@ -66,9 +66,9 @@ These dependencies are only required if you are not using a package manager - Visit the downloads list on [SourceForge](https://sourceforge.net/projects/boost/files/boost-binaries/). - Select the latest version from the list. - Download the `.exe` file appropriate to your Visual Studio installation version and system bitness (choose `-64` for 64-bit systems). - Visual Studio versions map as follows: `14.3` in the filename corresponds to MSVC 2022,`14.2` to 2019, `14.1` to 2017, `14.0` to 2015. _Anything prior to Visual Studio 2015 is unsupported. Please upgrade should you have an older installation._ + Visual Studio versions map as follows: `14.3` in the filename corresponds to MSVC 2022. _Anything prior to Visual Studio 2022 is unsupported. Please upgrade should you have an older installation._ - **Convenience link for Visual Studio 2022: [boost_1_79_0-msvc-14.3-64.exe](https://sourceforge.net/projects/boost/files/boost-binaries/1.79.0/boost_1_79_0-msvc-14.3-64.exe/download)** + **Convenience link for Visual Studio 2022: [boost_1_84_0-msvc-14.3-64.exe](https://sourceforge.net/projects/boost/files/boost-binaries/1.84.0/boost_1_84_0-msvc-14.3-64.exe/download)** 2. When prompted where to install Boost, set the location to `C:\local\boost`. 3. After the installation finishes, rename the `C:\local\boost\lib64-msvc-14.3` (or similar) directory to simply `lib` (`C:\local\boost\lib`). @@ -237,7 +237,7 @@ Select the `CMake Applications > chatterino` configuration and add a new _Run Ex Now you can run the `chatterino | Debug` configuration. -If you want to run the portable version of Chatterino, create a file called `modes` inside of `build/bin` and +If you want to run the portable version of Chatterino, create a file called `modes` inside `build/bin` and write `portable` into it. #### Debugging diff --git a/BUILDING_ON_WINDOWS_WITH_VCPKG.md b/BUILDING_ON_WINDOWS_WITH_VCPKG.md index ec961572413..18748acb786 100644 --- a/BUILDING_ON_WINDOWS_WITH_VCPKG.md +++ b/BUILDING_ON_WINDOWS_WITH_VCPKG.md @@ -1,6 +1,6 @@ # Building on Windows with vcpkg -This will require more than 30GB of free space on your hard drive. +This will require more than 30 GB of free space on your hard drive. ## Prerequisites @@ -29,7 +29,7 @@ This will require more than 30GB of free space on your hard drive. See [VCPKG_ROOT documentation](https://learn.microsoft.com/en-gb/vcpkg/users/config-environment#vcpkg_root) - Append the vcpkg path to your path e.g. `setx PATH "%PATH%;"` - - For more configurations, see https://learn.microsoft.com/en-gb/vcpkg/users/config-environment + - For more configurations, see 1. You may need to restart your computer to ensure all your environment variables and what-not are loaded everywhere. ## Building diff --git a/CHANGELOG.md b/CHANGELOG.md index 35717f6af82..fa05a4478b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) - Dev: Refactor/unsingletonize `UserDataController`. (#5459) - Dev: Cleanup `BrowserExtension`. (#5465) +- Dev: Deprecate Qt 5.12. (#5396) ## 2.5.1 diff --git a/src/common/LinkParser.cpp b/src/common/LinkParser.cpp index 8ef1dc232b7..d64abed31cf 100644 --- a/src/common/LinkParser.cpp +++ b/src/common/LinkParser.cpp @@ -124,15 +124,6 @@ LinkParser::LinkParser(const QString &unparsedString) QStringView remaining(unparsedString); QStringView protocol(remaining); -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - QStringView wholeString(unparsedString); - const auto refFromView = [&](QStringView view) { - return QStringRef(&unparsedString, - static_cast(view.begin() - wholeString.begin()), - static_cast(view.size())); - }; -#endif - // Check protocol for https?:// if (remaining.startsWith(QStringLiteral("http"), Qt::CaseInsensitive) && remaining.length() >= 4 + 3 + 1) // 'http' + '://' + [any] @@ -149,12 +140,7 @@ LinkParser::LinkParser(const QString &unparsedString) { // there's really a protocol => consume it remaining = withProto.mid(3); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) result.protocol = {protocol.begin(), remaining.begin()}; -#else - result.protocol = - refFromView({protocol.begin(), remaining.begin()}); -#endif } } @@ -219,13 +205,8 @@ LinkParser::LinkParser(const QString &unparsedString) if ((nDots == 3 && isValidIpv4(host)) || isValidTld(host.mid(lastDotPos + 1))) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) result.host = host; result.rest = rest; -#else - result.host = refFromView(host); - result.rest = refFromView(rest); -#endif result.source = unparsedString; this->result_ = std::move(result); } diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 9d5e10cfb68..2ef1183181c 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -7,28 +7,23 @@ namespace chatterino { struct ParsedLink { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - using StringView = QStringView; -#else - using StringView = QStringRef; -#endif /// The parsed protocol of the link. Can be empty. /// /// https://www.forsen.tv/commands /// ^------^ - StringView protocol; + QStringView protocol; /// The parsed host of the link. Can not be empty. /// /// https://www.forsen.tv/commands /// ^-----------^ - StringView host; + QStringView host; /// The remainder of the link. Can be empty. /// /// https://www.forsen.tv/commands /// ^-------^ - StringView rest; + QStringView rest; /// The original unparsed link. /// diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index af96ee9ee90..ac4dca9e846 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -40,7 +40,6 @@ #include "singletons/Paths.hpp" #include "util/CombinePath.hpp" #include "util/QStringHash.hpp" -#include "util/Qt.hpp" #include diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index ef0cb8c1c1b..64af873b9f9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -15,7 +15,6 @@ #include "singletons/Resources.hpp" #include "singletons/Theme.hpp" #include "util/FormatTime.hpp" -#include "util/Qt.hpp" #include diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index 98ec30473f4..52349107f2a 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -14,7 +14,6 @@ #include "singletons/StreamerMode.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" -#include "util/Qt.hpp" #include diff --git a/src/messages/search/AuthorPredicate.cpp b/src/messages/search/AuthorPredicate.cpp index adc3655778e..ae951b8cddf 100644 --- a/src/messages/search/AuthorPredicate.cpp +++ b/src/messages/search/AuthorPredicate.cpp @@ -1,7 +1,6 @@ #include "messages/search/AuthorPredicate.hpp" #include "messages/Message.hpp" -#include "util/Qt.hpp" namespace chatterino { diff --git a/src/messages/search/BadgePredicate.cpp b/src/messages/search/BadgePredicate.cpp index dc3fbafa8cd..218ede8e1af 100644 --- a/src/messages/search/BadgePredicate.cpp +++ b/src/messages/search/BadgePredicate.cpp @@ -2,7 +2,6 @@ #include "messages/Message.hpp" #include "providers/twitch/TwitchBadge.hpp" -#include "util/Qt.hpp" namespace chatterino { diff --git a/src/messages/search/ChannelPredicate.cpp b/src/messages/search/ChannelPredicate.cpp index 665f96a973e..47df247fa80 100644 --- a/src/messages/search/ChannelPredicate.cpp +++ b/src/messages/search/ChannelPredicate.cpp @@ -1,7 +1,6 @@ #include "messages/search/ChannelPredicate.hpp" #include "messages/Message.hpp" -#include "util/Qt.hpp" namespace chatterino { diff --git a/src/messages/search/LinkPredicate.cpp b/src/messages/search/LinkPredicate.cpp index 69442069c9a..8430f84ba11 100644 --- a/src/messages/search/LinkPredicate.cpp +++ b/src/messages/search/LinkPredicate.cpp @@ -2,7 +2,6 @@ #include "common/LinkParser.hpp" #include "messages/Message.hpp" -#include "util/Qt.hpp" namespace chatterino { diff --git a/src/messages/search/MessageFlagsPredicate.cpp b/src/messages/search/MessageFlagsPredicate.cpp index 76e32de7209..d36fc72bb1d 100644 --- a/src/messages/search/MessageFlagsPredicate.cpp +++ b/src/messages/search/MessageFlagsPredicate.cpp @@ -1,7 +1,5 @@ #include "messages/search/MessageFlagsPredicate.hpp" -#include "util/Qt.hpp" - namespace chatterino { MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate) diff --git a/src/messages/search/SubtierPredicate.cpp b/src/messages/search/SubtierPredicate.cpp index 2dc79fc3518..70d5b7148cf 100644 --- a/src/messages/search/SubtierPredicate.cpp +++ b/src/messages/search/SubtierPredicate.cpp @@ -2,7 +2,6 @@ #include "messages/Message.hpp" #include "providers/twitch/TwitchBadge.hpp" -#include "util/Qt.hpp" namespace chatterino { diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 7fa77e4947a..fe7e449909b 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -37,7 +37,6 @@ #include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" #include "util/QStringHash.hpp" -#include "util/Qt.hpp" #include "widgets/Window.hpp" #include @@ -382,13 +381,7 @@ namespace { dst.reserve(newLength); for (const QStringView &chunk : std::as_const(chunks)) { -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2) - static_assert(sizeof(QChar) == sizeof(decltype(*chunk.utf16()))); - dst.append(reinterpret_cast(chunk.utf16()), - chunk.length()); -#else dst += chunk; -#endif } return dst; } @@ -1178,13 +1171,8 @@ void TwitchMessageBuilder::processIgnorePhrases( shiftIndicesAfter(static_cast(from + length), static_cast(replacement.length() - length)); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto midExtendedRef = QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart); -#else - auto midExtendedRef = - originalMessage.midRef(wordStart, wordEnd - wordStart); -#endif for (auto &emote : removedEmotes) { diff --git a/src/util/DebugCount.cpp b/src/util/DebugCount.cpp index 1fe915cf2f5..1c390678209 100644 --- a/src/util/DebugCount.cpp +++ b/src/util/DebugCount.cpp @@ -85,11 +85,7 @@ void DebugCount::decrease(const QString &name, const int64_t &amount) QString DebugCount::getDebugText() { -#if QT_VERSION > QT_VERSION_CHECK(5, 13, 0) static const QLocale locale(QLocale::English); -#else - static QLocale locale(QLocale::English); -#endif auto counts = COUNTS.access(); diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index f81ac9b792d..7d66184d192 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -11,7 +11,7 @@ namespace chatterino { namespace _helpers_internal { - SizeType skipSpace(StringView view, SizeType startPos) + SizeType skipSpace(QStringView view, SizeType startPos) { while (startPos < view.length() && view.at(startPos).isSpace()) { @@ -20,7 +20,7 @@ namespace _helpers_internal { return startPos - 1; } - bool matchesIgnorePlural(StringView word, const QString &expected) + bool matchesIgnorePlural(QStringView word, const QString &expected) { if (!word.startsWith(expected)) { @@ -34,7 +34,7 @@ namespace _helpers_internal { word.at(word.length() - 1).toLatin1() == 's'; } - std::pair findUnitMultiplierToSec(StringView view, + std::pair findUnitMultiplierToSec(QStringView view, SizeType &pos) { // Step 1. find end of unit @@ -207,11 +207,7 @@ int64_t parseDurationToSeconds(const QString &inputString, return -1; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) - StringView input(inputString); -#else - StringView input(&inputString); -#endif + QStringView input(inputString); input = input.trimmed(); uint64_t currentValue = 0; diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 56e25082a9c..6f0552487be 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -4,10 +4,6 @@ #include #include -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2) -# include -#endif - #include #include #include @@ -17,12 +13,7 @@ namespace chatterino { // only qualified for tests namespace _helpers_internal { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) - using StringView = QStringView; -#else - using StringView = QStringRef; -#endif - using SizeType = StringView::size_type; + using SizeType = QStringView::size_type; /** * Skips all spaces. @@ -32,7 +23,7 @@ namespace _helpers_internal { * @param startPos The starting position (there must be a space in the view). * @return The position of the last space. */ - SizeType skipSpace(StringView view, SizeType startPos); + SizeType skipSpace(QStringView view, SizeType startPos); /** * Checks if `word` equals `expected` (singular) or `expected` + 's' (plural). @@ -41,7 +32,7 @@ namespace _helpers_internal { * @param expected Singular of the expected word. * @return true if `word` is singular or plural of `expected`. */ - bool matchesIgnorePlural(StringView word, const QString &expected); + bool matchesIgnorePlural(QStringView word, const QString &expected); /** * Tries to find the unit starting at `pos` and returns its multiplier so @@ -58,7 +49,7 @@ namespace _helpers_internal { * if it's a valid unit, undefined otherwise. * @return (multiplier, ok) */ - std::pair findUnitMultiplierToSec(StringView view, + std::pair findUnitMultiplierToSec(QStringView view, SizeType &pos); } // namespace _helpers_internal diff --git a/src/util/QStringHash.hpp b/src/util/QStringHash.hpp index eb4efe2f04c..ac4bef57aaf 100644 --- a/src/util/QStringHash.hpp +++ b/src/util/QStringHash.hpp @@ -4,8 +4,6 @@ #include #include -#include - namespace boost { template <> @@ -17,17 +15,3 @@ struct hash { }; } // namespace boost - -namespace std { - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -template <> -struct hash { - std::size_t operator()(const QString &s) const - { - return qHash(s); - } -}; -#endif - -} // namespace std diff --git a/src/util/Qt.hpp b/src/util/Qt.hpp deleted file mode 100644 index 1187953d820..00000000000 --- a/src/util/Qt.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -namespace Qt { -const QString::SplitBehavior SkipEmptyParts = QString::SkipEmptyParts; -} -#endif diff --git a/src/util/XDGDirectory.cpp b/src/util/XDGDirectory.cpp index 979e58170c8..ab401e50fb2 100644 --- a/src/util/XDGDirectory.cpp +++ b/src/util/XDGDirectory.cpp @@ -1,7 +1,6 @@ #include "util/XDGDirectory.hpp" #include "util/CombinePath.hpp" -#include "util/Qt.hpp" #include diff --git a/src/util/XDGHelper.cpp b/src/util/XDGHelper.cpp index 588c4616648..e6e2df63fc9 100644 --- a/src/util/XDGHelper.cpp +++ b/src/util/XDGHelper.cpp @@ -3,7 +3,6 @@ #include "common/Literals.hpp" #include "common/QLogging.hpp" #include "util/CombinePath.hpp" -#include "util/Qt.hpp" #include "util/XDGDesktopFile.hpp" #include "util/XDGDirectory.hpp" diff --git a/src/widgets/helper/ResizingTextEdit.cpp b/src/widgets/helper/ResizingTextEdit.cpp index 3a44aa3e4e2..0ac7e0e8957 100644 --- a/src/widgets/helper/ResizingTextEdit.cpp +++ b/src/widgets/helper/ResizingTextEdit.cpp @@ -77,11 +77,7 @@ QString ResizingTextEdit::textUnderCursor(bool *hadSpace) const auto textUpToCursor = currentText.left(tc.selectionStart()); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto words = QStringView{textUpToCursor}.split(' '); -#else - auto words = textUpToCursor.splitRef(' '); -#endif if (words.size() == 0) { return QString(); diff --git a/src/widgets/settingspages/CommandPage.cpp b/src/widgets/settingspages/CommandPage.cpp index 9dc53b9bf19..de94d11d281 100644 --- a/src/widgets/settingspages/CommandPage.cpp +++ b/src/widgets/settingspages/CommandPage.cpp @@ -7,7 +7,6 @@ #include "singletons/Settings.hpp" #include "util/CombinePath.hpp" #include "util/LayoutCreator.hpp" -#include "util/Qt.hpp" #include "util/StandardItemHelper.hpp" #include "widgets/helper/EditableModelView.hpp" diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index 141eeed5e6b..865392163e3 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -226,20 +226,16 @@ void SplitInput::themeChangedEvent() QPalette 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->updateCancelReplyButton(); this->ui_.textEditLength->setPalette(palette); this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) this->ui_.textEdit->setPalette(placeholderPalette); -#endif auto marginPx = static_cast(2.F * this->scale()); this->ui_.vbox->setContentsMargins(marginPx, marginPx, marginPx, marginPx); diff --git a/tests/src/Helpers.cpp b/tests/src/Helpers.cpp index c615167cff7..a98868bfa7e 100644 --- a/tests/src/Helpers.cpp +++ b/tests/src/Helpers.cpp @@ -252,12 +252,6 @@ TEST(Helpers, BatchDifferentInputType) EXPECT_EQ(result, expectation); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) -# define makeView(x) x -#else -# define makeView(str) (&(str)) -#endif - TEST(Helpers, skipSpace) { struct TestCase { @@ -272,7 +266,7 @@ TEST(Helpers, skipSpace) for (const auto &c : tests) { - const auto actual = skipSpace(makeView(c.input), c.startIdx); + const auto actual = skipSpace(c.input, c.startIdx); EXPECT_EQ(actual, c.expected) << actual << " (" << c.input << ") did not match expected value " @@ -414,7 +408,7 @@ TEST(Helpers, findUnitMultiplierToSec) for (const auto &c : tests) { SizeType pos = c.startPos; - const auto actual = findUnitMultiplierToSec(makeView(c.input), pos); + const auto actual = findUnitMultiplierToSec(c.input, pos); if (c.expectedMultiplier == bad) { From 7bfb5ac0a45a798b17490fa5d485d57809b1599b Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 23 Jun 2024 19:45:57 +0200 Subject: [PATCH 105/105] dev: remove macos artifact from nightly release (#5483) --- .github/workflows/build.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc402fd7448..6b757e885cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -380,18 +380,9 @@ jobs: cp .CI/chatterino-nightly.flatpakref release-artifacts/ shell: bash - # macOS - - uses: actions/download-artifact@v4 - name: macOS x86_64 Qt5.15.2 dmg - with: - name: chatterino-macos-Qt-5.15.2.dmg - path: release-artifacts/ - - name: Rename artifacts run: | ls -l - # Rename the macos build to indicate that it's for macOS 10.15 users - mv chatterino-macos-Qt-5.15.2.dmg Chatterino-macOS-10.15.dmg # Mark all Windows Qt5 builds as old mv chatterino-windows-x86-64-Qt-5.15.2.zip chatterino-windows-old-x86-64-Qt-5.15.2.zip