From c1326e9b479645bd54b460cdc43fb6f347fa84f7 Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Sun, 3 Mar 2024 18:56:01 -0500 Subject: [PATCH 01/16] chore(deps): update ccache-action back to upstream repo (#5233) --- .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 9d5c1edd3cd..bf692e83f08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -197,7 +197,7 @@ jobs: - name: Setup sccache (Windows) # sccache v0.5.3 - uses: nerixyz/ccache-action@9a7e8d00116ede600ee7717350c6594b8af6aaa5 + uses: hendrikmuhs/ccache-action@v1.2.12 if: startsWith(matrix.os, 'windows') with: variant: sccache diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e47938ac369..6b3c434075e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -73,7 +73,7 @@ jobs: - name: Setup sccache # sccache v0.5.3 - uses: nerixyz/ccache-action@9a7e8d00116ede600ee7717350c6594b8af6aaa5 + uses: hendrikmuhs/ccache-action@v1.2.12 with: variant: sccache # only save on the default (master) branch From 6bc34ca97abe1ff285b5d67e2dbe5c5269e3c81a Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Sun, 3 Mar 2024 22:32:52 -0500 Subject: [PATCH 02/16] chore(deps): update sccache version comments (#5234) --- .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 bf692e83f08..d1581eddf07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -196,7 +196,7 @@ jobs: shell: powershell - name: Setup sccache (Windows) - # sccache v0.5.3 + # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 if: startsWith(matrix.os, 'windows') with: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b3c434075e..12ddb164cd5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -72,7 +72,7 @@ jobs: "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" - name: Setup sccache - # sccache v0.5.3 + # sccache v0.7.4 uses: hendrikmuhs/ccache-action@v1.2.12 with: variant: sccache From 9151446c0e1ea58815a8a8a444ecf382cbdd412d Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 6 Mar 2024 19:01:42 +0100 Subject: [PATCH 03/16] fix(streamer-mode): check setting on startup (#5236) --- CHANGELOG.md | 2 +- src/singletons/StreamerMode.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2311c7bf7cc..9858a65c02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -170,7 +170,7 @@ - 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) -- Dev: Refactor `StreamerMode`. (#5216) +- Dev: Refactor `StreamerMode`. (#5216, #5236) - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) ## 2.4.6 diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp index 06bdafebce0..cb73112757f 100644 --- a/src/singletons/StreamerMode.cpp +++ b/src/singletons/StreamerMode.cpp @@ -163,7 +163,6 @@ bool StreamerMode::isEnabled() const StreamerModePrivate::StreamerModePrivate(StreamerMode *parent) : parent_(parent) { - this->thread_.start(); this->timer_.moveToThread(&this->thread_); QObject::connect(&this->timer_, &QTimer::timeout, [this] { auto timeouts = @@ -184,6 +183,11 @@ StreamerModePrivate::StreamerModePrivate(StreamerMode *parent) }); }, this->settingConnections_); + + QObject::connect(&this->thread_, &QThread::started, [this] { + this->settingChanged(getSettings()->enableStreamerMode.getEnum()); + }); + this->thread_.start(); } bool StreamerModePrivate::isEnabled() const From 8cea86cf17eecfe971182bf51da8ccd00f20d2f1 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 9 Mar 2024 11:22:23 +0100 Subject: [PATCH 04/16] Fix rerun flag not being unset after stream finishes (#5237) --- CHANGELOG.md | 2 +- .../commands/CommandController.cpp | 2 ++ src/controllers/commands/builtin/Misc.cpp | 23 +++++++++++++++++++ src/controllers/commands/builtin/Misc.hpp | 1 + src/providers/twitch/TwitchChannel.cpp | 5 ++++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9858a65c02a..a1d93b7d0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ - Minor: Add a new Channel API for experimental plugins feature. (#5141, #5184, #5187) - 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) +- 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: Allow theming of tab live and rerun indicators. (#5188) - 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) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index dd9025fb77f..35cd4be0108 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -384,6 +384,8 @@ void CommandController::initialize(Settings &, const Paths &paths) #ifndef NDEBUG this->registerCommand("/fakemsg", &commands::injectFakeMessage); + this->registerCommand("/debug-update-to-no-stream", + &commands::injectStreamUpdateNoStream); #endif this->registerCommand("/copy", &commands::copyToClipboard); diff --git a/src/controllers/commands/builtin/Misc.cpp b/src/controllers/commands/builtin/Misc.cpp index 75a87d37df6..c0b62c2d9f9 100644 --- a/src/controllers/commands/builtin/Misc.cpp +++ b/src/controllers/commands/builtin/Misc.cpp @@ -29,6 +29,8 @@ #include #include +#include + namespace chatterino::commands { QString follow(const CommandContext &ctx) @@ -569,6 +571,27 @@ QString injectFakeMessage(const CommandContext &ctx) return ""; } +QString injectStreamUpdateNoStream(const CommandContext &ctx) +{ + /** + * /debug-update-to-no-stream makes the current channel mimic going offline + */ + if (ctx.channel == nullptr) + { + return ""; + } + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addMessage( + makeSystemMessage("The /debug-update-to-no-stream command only " + "works in Twitch channels")); + return ""; + } + + ctx.twitchChannel->updateStreamStatus(std::nullopt); + return ""; +} + QString copyToClipboard(const CommandContext &ctx) { if (ctx.channel == nullptr) diff --git a/src/controllers/commands/builtin/Misc.hpp b/src/controllers/commands/builtin/Misc.hpp index 7a8be28c798..7d1589266da 100644 --- a/src/controllers/commands/builtin/Misc.hpp +++ b/src/controllers/commands/builtin/Misc.hpp @@ -25,6 +25,7 @@ QString clearmessages(const CommandContext &ctx); QString openURL(const CommandContext &ctx); QString sendRawMessage(const CommandContext &ctx); QString injectFakeMessage(const CommandContext &ctx); +QString injectStreamUpdateNoStream(const CommandContext &ctx); QString copyToClipboard(const CommandContext &ctx); QString unstableSetUserClientSideColor(const CommandContext &ctx); QString openUsercard(const CommandContext &ctx); diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index b5a368a0b33..39350b69c12 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1178,6 +1178,11 @@ bool TwitchChannel::setLive(bool newLiveStatus) return false; } guard->live = newLiveStatus; + if (!newLiveStatus) + { + // A rerun is just a fancy livestream + guard->rerun = false; + } return true; } From 26bb4e236d6bcb244a83abbc7b2f1558fc7bd862 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 9 Mar 2024 11:25:20 +0100 Subject: [PATCH 05/16] fix(tooltips): hide image label by default (#5232) --- CHANGELOG.md | 1 + src/widgets/TooltipEntryWidget.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d93b7d0f8..ca00c7a07ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ - 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 split header tooltips appearing too tall. (#5232) - 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/TooltipEntryWidget.cpp b/src/widgets/TooltipEntryWidget.cpp index ff56928125e..7ef7274e385 100644 --- a/src/widgets/TooltipEntryWidget.cpp +++ b/src/widgets/TooltipEntryWidget.cpp @@ -24,6 +24,7 @@ TooltipEntryWidget::TooltipEntryWidget(ImagePtr image, const QString &text, this->displayImage_ = new QLabel(); this->displayImage_->setAlignment(Qt::AlignHCenter); this->displayImage_->setStyleSheet("background: transparent"); + this->displayImage_->hide(); this->displayText_ = new QLabel(text); this->displayText_->setAlignment(Qt::AlignHCenter); this->displayText_->setStyleSheet("background: transparent"); From ecad4b052a436c451d7a4d16a92e0f0b1faa2a05 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 9 Mar 2024 11:27:42 +0100 Subject: [PATCH 06/16] fix(windows): show split tooltip before move (#5230) --- CHANGELOG.md | 1 + src/widgets/splits/SplitHeader.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca00c7a07ee..c19f6056abd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ - 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 split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - 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/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 8f105320f01..7e27ad16b17 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -950,13 +950,27 @@ void SplitHeader::enterEvent(QEvent *event) this->tooltipWidget_->setOne({nullptr, this->tooltipText_}); this->tooltipWidget_->setWordWrap(true); this->tooltipWidget_->adjustSize(); + + // On Windows, a lot of the resizing/activating happens when calling + // show() and calling it doesn't synchronously create a visible window, + // so moving the window won't cause the visible window to jump. + // + // On other platforms, this isn't the case, hence we call show() after + // moving. +#ifdef Q_OS_WIN + this->tooltipWidget_->show(); +#endif + auto pos = this->mapToGlobal(this->rect().bottomLeft()) + QPoint((this->width() - this->tooltipWidget_->width()) / 2, 1); this->tooltipWidget_->moveTo(pos, widgets::BoundsChecking::CursorPosition); + +#ifndef Q_OS_WIN this->tooltipWidget_->show(); +#endif } BaseWidget::enterEvent(event); From c50791972d8134ca26dfcb85b2ae0d5fc5f4fe3b Mon Sep 17 00:00:00 2001 From: KleberPF <43550602+KleberPF@users.noreply.github.com> Date: Sat, 9 Mar 2024 08:03:36 -0300 Subject: [PATCH 07/16] Add highlight color and show in mentions to automod messages (#5215) --- CHANGELOG.md | 2 ++ src/Application.cpp | 8 ++++++++ .../highlights/HighlightController.cpp | 5 ++++- src/controllers/highlights/HighlightModel.cpp | 18 +++++++++++++++--- src/controllers/highlights/HighlightPhrase.cpp | 1 + src/controllers/highlights/HighlightPhrase.hpp | 1 + src/messages/Message.cpp | 8 ++++++++ src/messages/Message.hpp | 12 +++++++----- src/messages/layouts/MessageLayout.cpp | 14 +++++++++++++- src/providers/colors/ColorProvider.cpp | 3 +++ src/providers/colors/ColorProvider.hpp | 1 + src/providers/twitch/TwitchMessageBuilder.cpp | 1 + src/singletons/Settings.hpp | 5 +++++ 13 files changed, 69 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c19f6056abd..731d39a4bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ - 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: 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: Image links now reflect the scale of their image instead of an internal label. (#5201) diff --git a/src/Application.cpp b/src/Application.cpp index 929bedd08b6..bede3f43388 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -867,6 +867,14 @@ void Application::initPubSub() p.first); getApp()->twitch->automodChannel->addMessage( p.second); + + if (getSettings()->showAutomodInMentions) + { + getApp()->twitch->mentionsChannel->addMessage( + p.first); + getApp()->twitch->mentionsChannel->addMessage( + p.second); + } }); } // "ALLOWED" and "DENIED" statuses remain unimplemented diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index 53062d08cae..3e15d7580f1 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -213,6 +213,8 @@ void rebuildMessageHighlights(Settings &settings, settings.enableAutomodHighlightTaskbar.getValue(); const auto highlightSoundUrlValue = settings.automodHighlightSoundUrl.getValue(); + auto highlightColor = + ColorProvider::instance().color(ColorType::AutomodHighlight); checks.emplace_back(HighlightCheck{ [=](const auto & /*args*/, const auto & /*badges*/, @@ -234,7 +236,7 @@ void rebuildMessageHighlights(Settings &settings, highlightAlert, // alert highlightSound, // playSound highlightSoundUrl, // customSoundUrl - nullptr, // color + highlightColor, // color false, // showInMentions }; }}); @@ -471,6 +473,7 @@ void HighlightController::initialize(Settings &settings, this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions); this->rebuildListener_.addSetting(settings.enableAutomodHighlight); + this->rebuildListener_.addSetting(settings.showAutomodInMentions); this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound); this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar); this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl); diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index c83657c86d6..97465311510 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -238,9 +238,10 @@ void HighlightModel::afterInit() const std::vector automodRow = this->createRow(); setBoolItem(automodRow[Column::Pattern], getSettings()->enableAutomodHighlight.getValue(), true, false); + setBoolItem(automodRow[Column::ShowInMentions], + getSettings()->showAutomodInMentions.getValue(), true, false); automodRow[Column::Pattern]->setData("AutoMod Caught Messages", Qt::DisplayRole); - automodRow[Column::ShowInMentions]->setFlags({}); setBoolItem(automodRow[Column::FlashTaskbar], getSettings()->enableAutomodHighlightTaskbar.getValue(), true, false); @@ -253,8 +254,9 @@ void HighlightModel::afterInit() const auto automodSound = QUrl(getSettings()->automodHighlightSoundUrl.getValue()); setFilePathItem(automodRow[Column::SoundPath], automodSound, false); - - automodRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags); + auto automodColor = + ColorProvider::instance().color(ColorType::AutomodHighlight); + setColorItem(automodRow[Column::Color], *automodColor, false); this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow); } @@ -322,6 +324,11 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->showThreadHighlightInMentions.setValue( value.toBool()); } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + getSettings()->showAutomodInMentions.setValue( + value.toBool()); + } } } break; @@ -502,6 +509,11 @@ void HighlightModel::customRowSetData(const std::vector &row, setColor(getSettings()->threadHighlightColor, ColorType::ThreadMessageHighlight); } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + setColor(getSettings()->automodHighlightColor, + ColorType::AutomodHighlight); + } } } break; diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index c8963e59c39..7afd1a33b3c 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -21,6 +21,7 @@ QColor HighlightPhrase::FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR = QColor HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR = QColor(143, 48, 24, 60); QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100); +QColor HighlightPhrase::FALLBACK_AUTOMOD_HIGHLIGHT_COLOR = QColor(64, 64, 64); bool HighlightPhrase::operator==(const HighlightPhrase &other) const { diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index d470d35f6a9..ed03fbe9614 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -86,6 +86,7 @@ class HighlightPhrase static QColor FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR; static QColor FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR; static QColor FALLBACK_THREAD_HIGHLIGHT_COLOR; + static QColor FALLBACK_AUTOMOD_HIGHLIGHT_COLOR; private: QString pattern_; diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index 73f8ccde038..8840c6919fa 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -69,6 +69,14 @@ ScrollbarHighlight Message::getScrollBarHighlight() const }; } + if (this->flags.has(MessageFlag::AutoModOffendingMessage) || + this->flags.has(MessageFlag::AutoModOffendingMessageHeader)) + { + return { + ColorProvider::instance().color(ColorType::AutomodHighlight), + }; + } + return {}; } diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 73c7640a4e7..b9e0b2321ec 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -51,15 +51,17 @@ enum class MessageFlag : int64_t { LiveUpdatesAdd = (1LL << 28), LiveUpdatesRemove = (1LL << 29), LiveUpdatesUpdate = (1LL << 30), + /// The header of a message caught by AutoMod containing allow/disallow + AutoModOffendingMessageHeader = (1LL << 31), /// The message caught by AutoMod containing the user who sent the message & its contents - AutoModOffendingMessage = (1LL << 31), - LowTrustUsers = (1LL << 32), + AutoModOffendingMessage = (1LL << 32), + LowTrustUsers = (1LL << 33), /// The message is sent by a user marked as restricted with Twitch's "Low Trust"/"Suspicious User" feature - RestrictedMessage = (1LL << 33), + RestrictedMessage = (1LL << 34), /// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature - MonitoredMessage = (1LL << 34), + MonitoredMessage = (1LL << 35), /// The message is an ACTION message (/me) - Action = (1LL << 35), + Action = (1LL << 36), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index a7c5576394f..efc1d1b561f 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -373,7 +373,19 @@ void MessageLayout::updateBuffer(QPixmap *buffer, else if (this->message_->flags.has(MessageFlag::AutoMod) || this->message_->flags.has(MessageFlag::LowTrustUsers)) { - backgroundColor = QColor("#404040"); + if (ctx.preferences.enableAutomodHighlight && + (this->message_->flags.has(MessageFlag::AutoModOffendingMessage) || + this->message_->flags.has( + MessageFlag::AutoModOffendingMessageHeader))) + { + backgroundColor = blendColors( + backgroundColor, + *ctx.colorProvider.color(ColorType::AutomodHighlight)); + } + else + { + backgroundColor = QColor("#404040"); + } } else if (this->message_->flags.has(MessageFlag::Debug)) { diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp index 72039a1f150..017dd0d8331 100644 --- a/src/providers/colors/ColorProvider.cpp +++ b/src/providers/colors/ColorProvider.cpp @@ -128,6 +128,9 @@ void ColorProvider::initTypeColorMap() initColor(ColorType::ThreadMessageHighlight, getSettings()->threadHighlightColor, HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR); + + initColor(ColorType::AutomodHighlight, getSettings()->automodHighlightColor, + HighlightPhrase::FALLBACK_AUTOMOD_HIGHLIGHT_COLOR); } void ColorProvider::initDefaultColors() diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp index dfd9b4b838c..c35da054323 100644 --- a/src/providers/colors/ColorProvider.hpp +++ b/src/providers/colors/ColorProvider.hpp @@ -18,6 +18,7 @@ enum class ColorType { ThreadMessageHighlight, // Used in automatic highlights of your own messages SelfMessageHighlight, + AutomodHighlight, }; class ColorProvider diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 8f416b8ee40..af266889c0b 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -2009,6 +2009,7 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( builder.message().flags.set(MessageFlag::PubSub); builder.message().flags.set(MessageFlag::Timeout); builder.message().flags.set(MessageFlag::AutoMod); + builder.message().flags.set(MessageFlag::AutoModOffendingMessageHeader); // AutoMod shield badge builder.emplace(makeAutoModBadge(), diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index d96b13f0ec0..3e0c693f228 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -394,6 +394,10 @@ class Settings "/highlighting/automod/enabled", true, }; + BoolSetting showAutomodInMentions = { + "/highlighting/automod/showInMentions", + false, + }; BoolSetting enableAutomodHighlightSound = { "/highlighting/automod/enableSound", false, @@ -406,6 +410,7 @@ class Settings "/highlighting/automod/soundUrl", "", }; + QStringSetting automodHighlightColor = {"/highlighting/automod/color", ""}; BoolSetting enableThreadHighlight = { "/highlighting/thread/nameIsHighlightKeyword", true}; From 2e77b47ea120d0bc1b044a651c1ba4239ee66828 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 9 Mar 2024 12:29:25 +0100 Subject: [PATCH 08/16] fix: settings "Cancel" button doesn't work first time (#5229) --- CHANGELOG.md | 1 + src/singletons/Settings.cpp | 5 +++++ src/widgets/dialogs/SettingsDialog.cpp | 11 +++-------- src/widgets/settingspages/SettingsPage.cpp | 5 ----- src/widgets/settingspages/SettingsPage.hpp | 3 --- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 731d39a4bce..e07661c8893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ - 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) - 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) diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 6cc603c20fd..b05c13409fe 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -8,6 +8,7 @@ #include "controllers/ignores/IgnorePhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" #include "controllers/nicknames/Nickname.hpp" +#include "debug/Benchmark.hpp" #include "util/Clamp.hpp" #include "util/PersistSignalVector.hpp" #include "util/WindowsHelper.hpp" @@ -195,6 +196,8 @@ Settings::~Settings() void Settings::saveSnapshot() { + BenchmarkGuard benchmark("Settings::saveSnapshot"); + rapidjson::Document *d = new rapidjson::Document(rapidjson::kObjectType); rapidjson::Document::AllocatorType &a = d->GetAllocator(); @@ -230,6 +233,8 @@ void Settings::restoreSnapshot() return; } + BenchmarkGuard benchmark("Settings::restoreSnapshot"); + const auto &snapshot = *(this->snapshot_.get()); if (!snapshot.IsObject()) diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index c8f7c3b49b8..49cdf8e3539 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -341,6 +341,9 @@ void SettingsDialog::showDialog(QWidget *parent, } hasShownBefore = true; + // Resets the cancel button. + getSettings()->saveSnapshot(); + switch (preferredTab) { case SettingsDialogPreference::Accounts: @@ -379,9 +382,6 @@ void SettingsDialog::showDialog(QWidget *parent, void SettingsDialog::refresh() { - // Resets the cancel button. - getSettings()->saveSnapshot(); - // Updates tabs. for (auto *tab : this->tabs_) { @@ -440,11 +440,6 @@ void SettingsDialog::onOkClicked() void SettingsDialog::onCancelClicked() { - for (auto &tab : this->tabs_) - { - tab->page()->cancel(); - } - getSettings()->restoreSnapshot(); this->close(); diff --git a/src/widgets/settingspages/SettingsPage.cpp b/src/widgets/settingspages/SettingsPage.cpp index c78bddee3dd..4acf26791b9 100644 --- a/src/widgets/settingspages/SettingsPage.cpp +++ b/src/widgets/settingspages/SettingsPage.cpp @@ -87,11 +87,6 @@ void SettingsPage::setTab(SettingsDialogTab *tab) this->tab_ = tab; } -void SettingsPage::cancel() -{ - this->onCancel_.invoke(); -} - QCheckBox *SettingsPage::createCheckBox( const QString &text, pajlada::Settings::Setting &setting) { diff --git a/src/widgets/settingspages/SettingsPage.hpp b/src/widgets/settingspages/SettingsPage.hpp index f982114cbf6..213836fb750 100644 --- a/src/widgets/settingspages/SettingsPage.hpp +++ b/src/widgets/settingspages/SettingsPage.hpp @@ -56,8 +56,6 @@ class SettingsPage : public QFrame SettingsDialogTab *tab() const; void setTab(SettingsDialogTab *tab); - void cancel(); - QCheckBox *createCheckBox(const QString &text, pajlada::Settings::Setting &setting); QComboBox *createComboBox(const QStringList &items, @@ -86,7 +84,6 @@ class SettingsPage : public QFrame protected: SettingsDialogTab *tab_{}; - pajlada::Signals::NoArgSignal onCancel_; pajlada::Signals::SignalHolder managedConnections_; }; From 2361d30e4be04df9b9fe970fbb185ed3d9a9331d Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 9 Mar 2024 16:03:26 +0100 Subject: [PATCH 09/16] fix: compare settings before updating them (#5240) --- CHANGELOG.md | 1 + lib/settings | 2 +- src/common/ChatterinoSetting.hpp | 7 +++++-- src/controllers/logging/ChannelLog.cpp | 5 +++++ src/singletons/Settings.cpp | 6 +++++- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e07661c8893..95b51e5decc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ - 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 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 some emotes not appearing when using _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) diff --git a/lib/settings b/lib/settings index 41ef7899b1c..ceac9c7e97d 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 41ef7899b1cce35982f2dac600b2034375574f49 +Subproject commit ceac9c7e97d2d2b97f40ecd0b421e358d7525cbc diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 2f5a0cac436..be3ebb8ff4a 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -13,13 +13,16 @@ class ChatterinoSetting : public pajlada::Settings::Setting { public: ChatterinoSetting(const std::string &path) - : pajlada::Settings::Setting(path) + : pajlada::Settings::Setting( + path, pajlada::Settings::SettingOption::CompareBeforeSet) { _registerSetting(this->getData()); } ChatterinoSetting(const std::string &path, const Type &defaultValue) - : pajlada::Settings::Setting(path, defaultValue) + : pajlada::Settings::Setting( + path, defaultValue, + pajlada::Settings::SettingOption::CompareBeforeSet) { _registerSetting(this->getData()); } diff --git a/src/controllers/logging/ChannelLog.cpp b/src/controllers/logging/ChannelLog.cpp index 2c163050be4..67925b94557 100644 --- a/src/controllers/logging/ChannelLog.cpp +++ b/src/controllers/logging/ChannelLog.cpp @@ -7,6 +7,11 @@ ChannelLog::ChannelLog(QString channelName) { } +bool ChannelLog::operator==(const ChannelLog &other) const +{ + return this->channelName_ == other.channelName_; +} + QString ChannelLog::channelName() const { return this->channelName_.toLower(); diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index b05c13409fe..451692aa037 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -9,6 +9,7 @@ #include "controllers/moderationactions/ModerationAction.hpp" #include "controllers/nicknames/Nickname.hpp" #include "debug/Benchmark.hpp" +#include "pajlada/settings/signalargs.hpp" #include "util/Clamp.hpp" #include "util/PersistSignalVector.hpp" #include "util/WindowsHelper.hpp" @@ -257,7 +258,10 @@ void Settings::restoreSnapshot() continue; } - setting->marshalJSON(snapshot[path]); + pajlada::Settings::SignalArgs args; + args.compareBeforeSet = true; + + setting->marshalJSON(snapshot[path], std::move(args)); } } From 658fceddaa23198c67119ab725dffaa8c8f3afd6 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 9 Mar 2024 20:16:25 +0100 Subject: [PATCH 10/16] Add plugin permissions and IO API (#5231) --- CHANGELOG.md | 2 + docs/plugin-info.schema.json | 14 +- docs/wip-plugins.md | 168 ++++++++- src/CMakeLists.txt | 4 + src/controllers/plugins/LuaAPI.cpp | 8 + src/controllers/plugins/LuaUtilities.hpp | 10 +- src/controllers/plugins/Plugin.cpp | 66 ++++ src/controllers/plugins/Plugin.hpp | 10 + src/controllers/plugins/PluginController.cpp | 52 ++- src/controllers/plugins/PluginPermission.cpp | 46 +++ src/controllers/plugins/PluginPermission.hpp | 30 ++ src/controllers/plugins/api/ChannelRef.cpp | 8 +- src/controllers/plugins/api/ChannelRef.hpp | 6 +- src/controllers/plugins/api/IOWrapper.cpp | 372 +++++++++++++++++++ src/controllers/plugins/api/IOWrapper.hpp | 98 +++++ src/widgets/settingspages/PluginsPage.cpp | 14 + 16 files changed, 889 insertions(+), 19 deletions(-) create mode 100644 src/controllers/plugins/PluginPermission.cpp create mode 100644 src/controllers/plugins/PluginPermission.hpp create mode 100644 src/controllers/plugins/api/IOWrapper.cpp create mode 100644 src/controllers/plugins/api/IOWrapper.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b51e5decc..92100b61e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ - 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) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) diff --git a/docs/plugin-info.schema.json b/docs/plugin-info.schema.json index 2b5203ef661..4642010025a 100644 --- a/docs/plugin-info.schema.json +++ b/docs/plugin-info.schema.json @@ -41,9 +41,21 @@ }, "license": { "type": "string", - "description": "A small description of your license.", + "description": "SPDX identifier for license of this plugin. See https://spdx.org/licenses/", "examples": ["MIT", "GPL-2.0-or-later"] }, + "permissions": { + "type": "array", + "description": "The permissions the plugin needs to work.", + "items": { + "type": "object", + "properties": { + "type": { + "enum": ["FilesystemRead", "FilesystemWrite"] + } + } + } + }, "$schema": { "type": "string" } }, "required": ["name", "description", "authors", "version", "license"] diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index ff5c221e796..1309d7bab0c 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -14,7 +14,9 @@ Each plugin should have its own directory. Chatterino Plugins dir/ └── plugin_name/ ├── init.lua - └── info.json + ├── info.json + └── data/ + └── This is where your data/configs can be dumped ``` `init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`require` global function](#requiremodname). @@ -35,12 +37,54 @@ Example file: "homepage": "https://github.com/Chatterino/Chatterino2", "tags": ["test"], "version": "0.0.0", - "license": "MIT" + "license": "MIT", + "permissions": [] } ``` An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin) +## Permissions + +Plugins can have permissions associated to them. Unless otherwise noted functions don't require permissions. +These are the valid permissions: + +### FilesystemRead + +Allows the plugin to read from its data directory. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "FilesystemRead" + }, + ... + ] +} +``` + +### FilesystemWrite + +Allows the plugin to write to files and create files in its data directory. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "FilesystemWrite" + }, + ... + ] +} +``` + ## Plugins with Typescript If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io) @@ -60,9 +104,10 @@ script](../scripts/make_luals_meta.py). The following parts of the Lua standard library are loaded: - `_G` (most globals) -- `table` -- `string` +- `io` - except `stdin`, `stdout`, `stderr`. Some functions require permissions. - `math` +- `string` +- `table` - `utf8` The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6). @@ -325,6 +370,117 @@ Returns `true` if the channel can be moderated by the current user. Returns `true` if the current user is a VIP in the channel. +### Input/Output API + +These functions are wrappers for Lua's I/O library. Functions on file pointer +objects (`FILE*`) are not modified or replaced. [You can read the documentation +for them here](https://www.lua.org/manual/5.4/manual.html#pdf-file:close). +Chatterino does _not_ give you stdin and stdout as default input and output +respectively. The following objects are missing from the `io` table exposed by +Chatterino compared to Lua's native library: `stdin`, `stdout`, `stderr`. + +#### `close([file])` + +Closes a file. If not given, `io.output()` is used instead. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.close) + +#### `flush()` + +Flushes `io.output()`. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.flush) + +#### `input([file_or_name])` + +When called with no arguments this function returns the default input file. +This variant requires no permissions. + +When called with a file object, it will set the default input file to the one +given. This one also requires no permissions. + +When called with a filename as a string, it will open that file for reading. +Equivalent to: `io.input(io.open(filename))`. This variant requires +the `FilesystemRead` permission and the given file to be within the plugin's +data directory. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.input) + +#### `lines([filename, ...])` + +With no arguments this function is equivalent to `io.input():lines("l")`. See +[Lua documentation for file:flush()](https://www.lua.org/manual/5.4/manual.html#pdf-file:flush). +This variant requires no permissions. + +With `filename` given it is most like `io.open(filename):lines(...)`. This +variant requires the `FilesystemRead` permission and the given file to be +within the plugin's data directory. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.lines) + +#### `open(filename [, mode])` + +This functions opens the given file with a mode. It requires `filename` to be +within the plugin's data directory. A call with no mode given is equivalent to +one with `mode="r"`. +Depending on the mode this function has slightly different behavior: + +| Mode | Permission | Read? | Write? | Truncate? | Create? | +| ----------- | ----------------- | ----- | ------ | --------- | ------- | +| `r` read | `FilesystemRead` | Yes | No | No | No | +| `w` write | `FilesystemWrite` | No | Yes | Yes | Yes | +| `a` append | `FilesystemWrite` | No | Append | No | Yes | +| `r+` update | `FilesystemWrite` | Yes | Yes | No | No | +| `w+` update | `FilesystemWrite` | Yes | Yes | Yes | Yes | +| `a+` update | `FilesystemWrite` | Yes | Append | No | Yes | + +To open a file in binary mode add a `b` at the end of the mode. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.open) + +#### `output([file_or_name])` + +This is identical to [`io.input()`](#inputfile_or_name) but operates on the +default output and opens the file in write mode instead. Requires +`FilesystemWrite` instead of `FilesystemRead`. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.output) + +#### `popen(exe [, mode])` + +This function is unavailable in Chatterino. Calling it results in an error +message to let you know that it's not available, no permissions needed. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.popen) + +#### `read(...)` + +Equivalent to `io.input():read(...)`. See [`io.input()`](#inputfile_or_name) +and [`file:read()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:read). + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.read) + +#### `tmpfile()` + +This function is unavailable in Chatterino. Calling it results in an error +message to let you know that it's not available, no permissions needed. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.tmpfile) + +#### `type(obj)` + +This functions allows you to tell if the object is a `file`, a `closed file` or +a different bit of data. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.type) + +#### `write(...)` + +Equivalent to `io.output():write(...)`. See [`io.output()`](#outputfile_or_name) +and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write). + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write) + ### Changed globals #### `load(chunk [, chunkname [, mode [, env]]])` @@ -344,7 +500,8 @@ However, the searcher and load configuration is notably different from the defau - `package.path` is not used, in its place are two searchers, - when `require()` is used, first a file relative to the currently executing file will be checked, then a file relative to the plugin directory, -- binary chunks are never loaded +- binary chunks are never loaded, +- files inside of the plugin `data` directory are never loaded As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows). @@ -354,6 +511,7 @@ Example: require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file +require("data.file") -- tried to load Plugins/name/data/file.lua and errors because that is not allowed ``` #### `print(Args...)` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 81c65832d0a..ba5e85b4d14 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -223,8 +223,12 @@ set(SOURCE_FILES controllers/plugins/api/ChannelRef.cpp controllers/plugins/api/ChannelRef.hpp + controllers/plugins/api/IOWrapper.cpp + controllers/plugins/api/IOWrapper.hpp controllers/plugins/LuaAPI.cpp controllers/plugins/LuaAPI.hpp + controllers/plugins/PluginPermission.cpp + controllers/plugins/PluginPermission.hpp controllers/plugins/Plugin.cpp controllers/plugins/Plugin.hpp controllers/plugins/PluginController.hpp diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp index 0e4d16b0aa9..291e95b22cd 100644 --- a/src/controllers/plugins/LuaAPI.cpp +++ b/src/controllers/plugins/LuaAPI.cpp @@ -266,6 +266,14 @@ int loadfile(lua_State *L, const QString &str) L, QString("requested module is outside of the plugin directory")); return 1; } + auto datadir = QUrl(pl->dataDirectory().canonicalPath() + "/"); + if (datadir.isParentOf(str)) + { + lua::push(L, QString("requested file is data, not code, see Chatterino " + "documentation")); + return 1; + } + QFileInfo info(str); if (!info.exists()) { diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 66ca1a9f1e5..f610ae25d32 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -283,6 +283,7 @@ StackIdx push(lua_State *L, T inp) /** * @brief Converts a Lua object into c++ and removes it from the stack. + * If peek fails, the object is still removed from the stack. * * Relies on bool peek(lua_State*, T*, StackIdx) existing. */ @@ -291,14 +292,11 @@ bool pop(lua_State *L, T *out, StackIdx idx = -1) { StackGuard guard(L, -1); auto ok = peek(L, out, idx); - if (ok) + if (idx < 0) { - if (idx < 0) - { - idx = lua_gettop(L) + idx + 1; - } - lua_remove(L, idx); + idx = lua_gettop(L) + idx + 1; } + lua_remove(L, idx); return ok; } diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index 63c69c388c2..562b6c07b6a 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -9,6 +9,7 @@ # include # include +# include # include # include @@ -111,6 +112,48 @@ PluginMeta::PluginMeta(const QJsonObject &obj) QString("version is not a string (its type is %1)").arg(type)); this->version = semver::version(0, 0, 0); } + auto permsObj = obj.value("permissions"); + if (!permsObj.isUndefined()) + { + if (!permsObj.isArray()) + { + QString type = magic_enum::enum_name(permsObj.type()).data(); + this->errors.emplace_back( + QString("permissions is not an array (its type is %1)") + .arg(type)); + return; + } + + auto permsArr = permsObj.toArray(); + for (int i = 0; i < permsArr.size(); i++) + { + const auto &t = permsArr.at(i); + if (!t.isObject()) + { + QString type = magic_enum::enum_name(t.type()).data(); + this->errors.push_back(QString("permissions element #%1 is not " + "an object (its type is %2)") + .arg(i) + .arg(type)); + return; + } + auto parsed = PluginPermission(t.toObject()); + if (parsed.isValid()) + { + // ensure no invalid permissions slip through this + this->permissions.push_back(parsed); + } + else + { + for (const auto &err : parsed.errors) + { + this->errors.push_back( + QString("permissions element #%1: %2").arg(i).arg(err)); + } + } + } + } + auto tagsObj = obj.value("tags"); if (!tagsObj.isUndefined()) { @@ -201,5 +244,28 @@ void Plugin::removeTimeout(QTimer *timer) } } +bool Plugin::hasFSPermissionFor(bool write, const QString &path) +{ + auto canon = QUrl(this->dataDirectory().absolutePath() + "/"); + if (!canon.isParentOf(path)) + { + return false; + } + + 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) + { + if (p.type == typ) + { + return true; + } + } + return false; +} + } // namespace chatterino #endif diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 67f9d35ffef..6dfb3b20e93 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -4,6 +4,7 @@ # include "Application.hpp" # include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginPermission.hpp" # include # include @@ -42,6 +43,8 @@ struct PluginMeta { // optionally tags that might help in searching for the plugin std::vector tags; + std::vector permissions; + // errors that occurred while parsing info.json std::vector errors; @@ -88,6 +91,11 @@ class Plugin return this->loadDirectory_; } + QDir dataDirectory() const + { + return this->loadDirectory_.absoluteFilePath("data"); + } + // Note: The CallbackFunction object's destructor will remove the function from the lua stack using LuaCompletionCallback = lua::CallbackFunctiondataDirectory().mkpath("."); + qCDebug(chatterinoLua) << "Running lua file:" << index; int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str()); if (err != 0) diff --git a/src/controllers/plugins/PluginPermission.cpp b/src/controllers/plugins/PluginPermission.cpp new file mode 100644 index 00000000000..d806db4bd30 --- /dev/null +++ b/src/controllers/plugins/PluginPermission.cpp @@ -0,0 +1,46 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/PluginPermission.hpp" + +# include +# include + +namespace chatterino { + +PluginPermission::PluginPermission(const QJsonObject &obj) +{ + auto jsontype = obj.value("type"); + if (!jsontype.isString()) + { + QString tn = magic_enum::enum_name(jsontype.type()).data(); + this->errors.emplace_back(QString("permission type is defined but is " + "not a string (its type is %1)") + .arg(tn)); + } + auto strtype = jsontype.toString().toStdString(); + auto opt = magic_enum::enum_cast( + strtype, magic_enum::case_insensitive); + if (!opt.has_value()) + { + this->errors.emplace_back(QString("permission type is an unknown (%1)") + .arg(jsontype.toString())); + return; // There is no more data to get, we don't know what to do + } + this->type = opt.value(); +} + +QString PluginPermission::toHtml() const +{ + switch (this->type) + { + case PluginPermission::Type::FilesystemRead: + return "Read files in its data directory"; + case PluginPermission::Type::FilesystemWrite: + return "Write to or create files in its data directory"; + default: + assert(false && "invalid PluginPermission type in toHtml()"); + return "shut up compiler, this never happens"; + } +} + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/PluginPermission.hpp b/src/controllers/plugins/PluginPermission.hpp new file mode 100644 index 00000000000..5867b7b63d6 --- /dev/null +++ b/src/controllers/plugins/PluginPermission.hpp @@ -0,0 +1,30 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS + +# include +# include + +# include + +namespace chatterino { + +struct PluginPermission { + explicit PluginPermission(const QJsonObject &obj); + + enum class Type { + FilesystemRead, + FilesystemWrite, + }; + Type type; + std::vector errors; + + bool isValid() const + { + return this->errors.empty(); + } + + QString toHtml() const; +}; + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index 3dc6af7f6fe..8ae91cd97c9 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -74,11 +74,13 @@ ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk) if (lua_isuserdata(L, lua_gettop(L)) == 0) { luaL_error( - L, "Called c2.Channel method with a non Channel 'self' argument."); + L, "Called c2.Channel method with a non-userdata 'self' argument"); return nullptr; } - auto *data = WeakPtrUserData::from( - lua_touserdata(L, lua_gettop(L))); + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel"); + auto *data = + WeakPtrUserData::from(checked); if (data == nullptr) { luaL_error(L, diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 2ec3295fa40..29f5173d28e 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -1,11 +1,11 @@ #pragma once -#include "providers/twitch/TwitchChannel.hpp" - -#include #ifdef CHATTERINO_HAVE_PLUGINS # include "common/Channel.hpp" # include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/PluginController.hpp" +# include "providers/twitch/TwitchChannel.hpp" + +# include namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) diff --git a/src/controllers/plugins/api/IOWrapper.cpp b/src/controllers/plugins/api/IOWrapper.cpp new file mode 100644 index 00000000000..7eeffaf71d7 --- /dev/null +++ b/src/controllers/plugins/api/IOWrapper.cpp @@ -0,0 +1,372 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/IOWrapper.hpp" + +# include "Application.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" +# include "lauxlib.h" +# include "lua.h" + +# include + +namespace chatterino::lua::api { + +// Note: Parsing and then serializing the mode ensures we understand it before +// passing it to Lua + +struct LuaFileMode { + char major = 'r'; // 'r'|'w'|'a' + bool update{}; // '+' + bool binary{}; // 'b' + QString error; + + LuaFileMode() = default; + + LuaFileMode(const QString &smode) + { + if (smode.isEmpty()) + { + this->error = "Empty mode given, use one matching /[rwa][+]?b?/."; + return; + } + auto major = smode.at(0); + if (major != 'r' && major != 'w' && major != 'a') + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 1st character."; + return; + } + this->major = major.toLatin1(); + if (smode.length() > 1) + { + auto plusOrB = smode.at(1); + if (plusOrB == '+') + { + this->update = true; + } + else if (plusOrB == 'b') + { + this->binary = true; + } + else + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 2nd character."; + return; + } + } + if (smode.length() > 2) + { + auto maybeB = smode.at(2); + if (maybeB == 'b') + { + this->binary = true; + } + else + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 3rd character."; + return; + } + } + } + + QString toString() const + { + assert(this->major == 'r' || this->major == 'w' || this->major == 'a'); + QString out; + out += this->major; + if (this->update) + { + out += '+'; + } + if (this->binary) + { + out += 'b'; + } + return out; + } +}; + +int ioError(lua_State *L, const QString &value, int errnoequiv) +{ + lua_pushnil(L); + lua::push(L, value); + lua::push(L, errnoequiv); + return 3; +} + +// NOLINTBEGIN(*vararg) +int io_open(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + LuaFileMode mode; + if (lua_gettop(L) == 2) + { + // we have a mode + QString smode; + if (!lua::pop(L, &smode)) + { + return luaL_error( + L, + "io.open mode (2nd argument) must be a string or not present"); + } + mode = LuaFileMode(smode); + if (!mode.error.isEmpty()) + { + return luaL_error(L, mode.error.toStdString().c_str()); + } + } + QString filename; + if (!lua::pop(L, &filename)) + { + return luaL_error(L, + "io.open filename (1st argument) must be a string"); + } + QFileInfo file(pl->dataDirectory().filePath(filename)); + auto abs = file.absoluteFilePath(); + qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name + << "] Plugin is opening file at " << abs + << " with mode " << mode.toString(); + bool ok = pl->hasFSPermissionFor( + mode.update || mode.major == 'w' || mode.major == 'a', abs); + if (!ok) + { + return ioError(L, + "Plugin does not have permissions to access given file.", + EACCES); + } + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "open"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua::push(L, abs); + lua::push(L, mode.toString()); + lua_call(L, 2, 3); + return 3; +} + +int io_lines(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + if (lua_gettop(L) == 0) + { + // io.lines() case, just call realio.lines + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "lines"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_call(L, 0, 1); + return 1; + } + QString filename; + if (!lua::pop(L, &filename)) + { + return luaL_error( + L, + "io.lines filename (1st argument) must be a string or not present"); + } + QFileInfo file(pl->dataDirectory().filePath(filename)); + auto abs = file.absoluteFilePath(); + qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name + << "] Plugin is opening file at " << abs + << " for reading lines"; + bool ok = pl->hasFSPermissionFor(false, abs); + if (!ok) + { + return ioError(L, + "Plugin does not have permissions to access given file.", + EACCES); + } + // Our stack looks like this: + // - {...}[1] + // - {...}[2] + // ... + // We want: + // - REG[REG_REAL_IO_NAME].lines + // - absolute file path + // - {...}[1] + // - {...}[2] + // ... + + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "lines"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_insert(L, 1); // move function to start of stack + lua::push(L, abs); + lua_insert(L, 2); // move file name just after the function + lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); + return lua_gettop(L); +} + +namespace { + + // This is the code for both io.input and io.output + int globalFileCommon(lua_State *L, bool output) + { + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + // Three signature cases: + // io.input() + // io.input(file) + // io.input(name) + if (lua_gettop(L) == 0) + { + // We have no arguments, call realio.input() + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + if (output) + { + lua_getfield(L, -1, "output"); + } + else + { + lua_getfield(L, -1, "input"); + } + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_call(L, 0, 1); + return 1; + } + if (lua_gettop(L) != 1) + { + return luaL_error(L, "Too many arguments given to io.input()."); + } + // Now check if we have a file or name + auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE); + if (p == nullptr) + { + // this is not a file handle, send it to open + luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME); + lua_getfield(L, -1, "open"); + lua_remove(L, -2); // remove io + + lua_pushvalue(L, 1); // dupe arg + if (output) + { + lua_pushstring(L, "w"); + } + else + { + lua_pushstring(L, "r"); + } + lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w') + // if this isn't a string ourio.open errors + + // this leaves us with: + // 1. arg + // 2. new_file + lua_remove(L, 1); // remove arg, replacing it with new_file + } + + // file handle, pass it off to realio.input + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + if (output) + { + lua_getfield(L, -1, "output"); + } + else + { + lua_getfield(L, -1, "input"); + } + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_pushvalue(L, 1); // duplicate arg + lua_call(L, 1, 1); + return 1; + } + +} // namespace + +int io_input(lua_State *L) +{ + return globalFileCommon(L, false); +} + +int io_output(lua_State *L) +{ + return globalFileCommon(L, true); +} + +int io_close(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.close. Expected one or zero."); + } + if (lua_gettop(L) == 0) + { + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + } + lua_getfield(L, -1, "close"); + lua_pushvalue(L, -2); + lua_call(L, 1, 0); + return 0; +} + +int io_flush(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.flush. Expected one or zero."); + } + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + lua_getfield(L, -1, "flush"); + lua_pushvalue(L, -2); + lua_call(L, 1, 0); + return 0; +} + +int io_read(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.read. Expected one or zero."); + } + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input"); + lua_getfield(L, -1, "read"); + lua_insert(L, 1); + lua_insert(L, 2); + lua_call(L, lua_gettop(L) - 1, 1); + return 1; +} + +int io_write(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + lua_getfield(L, -1, "write"); + lua_insert(L, 1); + lua_insert(L, 2); + // (input) + // (input).read + // args + lua_call(L, lua_gettop(L) - 1, 1); + return 1; +} + +int io_popen(lua_State *L) +{ + return luaL_error(L, "io.popen: This function is a stub!"); +} + +int io_tmpfile(lua_State *L) +{ + return luaL_error(L, "io.tmpfile: This function is a stub!"); +} + +// NOLINTEND(*vararg) + +} // namespace chatterino::lua::api +#endif diff --git a/src/controllers/plugins/api/IOWrapper.hpp b/src/controllers/plugins/api/IOWrapper.hpp new file mode 100644 index 00000000000..24ee2801ecf --- /dev/null +++ b/src/controllers/plugins/api/IOWrapper.hpp @@ -0,0 +1,98 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS + +struct lua_State; + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) +// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality. + +const char *const REG_REAL_IO_NAME = "real_lua_io_lib"; +const char *const REG_C2_IO_NAME = "c2io"; + +/** + * Opens a file. + * If given a relative path, it will be relative to + * c2datadir/Plugins/pluginDir/data/ + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.open + * + * @lua@param filename string + * @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+" + * @exposed io.open + */ +int io_open(lua_State *L); + +/** + * Equivalent to io.input():lines("l") or a specific iterator over given file + * If given a relative path, it will be relative to + * c2datadir/Plugins/pluginDir/data/ + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.lines + * + * @lua@param filename nil|string + * @lua@param ... + * @exposed io.lines + */ +int io_lines(lua_State *L); + +/** + * Opens a file and sets it as default input or if given no arguments returns the default input. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.input + * + * @lua@param fileorname nil|string|FILE* + * @lua@return nil|FILE* + * @exposed io.input + */ +int io_input(lua_State *L); + +/** + * Opens a file and sets it as default output or if given no arguments returns the default output + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.output + * + * @lua@param fileorname nil|string|FILE* + * @lua@return nil|FILE* + * @exposed io.output + */ +int io_output(lua_State *L); + +/** + * Closes given file or io.output() if not given. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.close + * + * @lua@param nil|FILE* + * @exposed io.close + */ +int io_close(lua_State *L); + +/** + * Flushes the buffer for given file or io.output() if not given. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.flush + * + * @lua@param nil|FILE* + * @exposed io.flush + */ +int io_flush(lua_State *L); + +/** + * Reads some data from the default input file + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.read + * + * @lua@param nil|string + * @exposed io.read + */ +int io_read(lua_State *L); + +/** + * Writes some data to the default output file + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.write + * + * @lua@param nil|string + * @exposed io.write + */ +int io_write(lua_State *L); + +int io_popen(lua_State *L); +int io_tmpfile(lua_State *L); + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api +#endif diff --git a/src/widgets/settingspages/PluginsPage.cpp b/src/widgets/settingspages/PluginsPage.cpp index 6fb710d5131..aad35f7513c 100644 --- a/src/widgets/settingspages/PluginsPage.cpp +++ b/src/widgets/settingspages/PluginsPage.cpp @@ -161,6 +161,20 @@ void PluginsPage::rebuildContent() } pluginEntry->addRow("Commands", new QLabel(commandsTxt, this->dataFrame_)); + if (!plugin->meta.permissions.empty()) + { + QString perms = "
    "; + for (const auto &perm : plugin->meta.permissions) + { + perms += "
  • " + perm.toHtml() + "
  • "; + } + perms += "
"; + + auto *lbl = + new QLabel("Required permissions:" + perms, this->dataFrame_); + lbl->setTextFormat(Qt::RichText); + pluginEntry->addRow(lbl); + } if (plugin->meta.isValid()) { From 2a2a7f47db74269d5d98158fb0479d8e9802e5b4 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 9 Mar 2024 23:18:19 +0100 Subject: [PATCH 11/16] Disable homebrew auto updating (#5242) --- .github/workflows/test-macos.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 530f1169bf0..d606c870880 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -9,6 +9,8 @@ on: env: TWITCH_PUBSUB_SERVER_TAG: v1.0.7 QT_QPA_PLATFORM: minimal + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 concurrency: group: test-macos-${{ github.ref }} From b9b1b8cf9cbb33618e445a100f0a9ef6e11a3a9b Mon Sep 17 00:00:00 2001 From: KleberPF <43550602+KleberPF@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:42:10 -0300 Subject: [PATCH 12/16] Add KleberPF to contributors list (#5241) --- resources/contributors.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/contributors.txt b/resources/contributors.txt index 380d3869cb6..b5907812ea8 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -68,6 +68,7 @@ chrrs | https://github.com/chrrs | | Contributor crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor SputNikPlop | https://github.com/SputNikPlop | | Contributor fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor +KleberPF | https://github.com/KleberPF | | Contributor # If you are a contributor add yourself above this line From 9d02fa14edcd67ce819221e047d6b4952123271f Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 10 Mar 2024 11:49:13 +0100 Subject: [PATCH 13/16] fix: Don't attempt to put the broadcaster username at the top (#5244) --- CHANGELOG.md | 2 +- src/controllers/completion/sources/UserSource.cpp | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92100b61e42..4606d64d23c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - Minor: Add a new completion API for experimental plugins feature. (#5000, #5047) - Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) - 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) +- 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) diff --git a/src/controllers/completion/sources/UserSource.cpp b/src/controllers/completion/sources/UserSource.cpp index bebe8183baf..d700ef25acd 100644 --- a/src/controllers/completion/sources/UserSource.cpp +++ b/src/controllers/completion/sources/UserSource.cpp @@ -67,16 +67,9 @@ void UserSource::initializeFromChannel(const Channel *channel) return user.first == tc->getName(); }); - if (it != this->items_.end()) + if (it == this->items_.end()) { - auto broadcaster = *it; - this->items_.erase(it); - this->items_.insert(this->items_.begin(), broadcaster); - } - else - { - this->items_.insert(this->items_.begin(), - {tc->getName(), tc->getDisplayName()}); + this->items_.emplace_back(tc->getName(), tc->getDisplayName()); } } } From a9586198603cc9a2d10bb5342d199691794727b4 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 10 Mar 2024 12:46:26 +0100 Subject: [PATCH 14/16] Hide chatter list button for non-moderators (#5245) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 33 +++++++++++---- src/widgets/splits/Split.cpp | 17 ++------ src/widgets/splits/SplitHeader.cpp | 47 +++++++++++++--------- src/widgets/splits/SplitHeader.hpp | 3 +- 5 files changed, 60 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4606d64d23c..7b4ea7dbeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - 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) +- Minor: The chatter list button is now hidden if you don't have moderator privileges. (#5245) - 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) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index b0eec1f8c6e..8a9cc9e6c2c 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -653,23 +653,40 @@ std::vector IrcMessageHandler::parseMessageWithReply( void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchIrcServer &server) { + auto chan = channelOrEmptyByTarget(message->target(), server); + if (chan->isEmpty()) + { + return; + } + + auto *twitchChannel = dynamic_cast(chan.get()); + + if (twitchChannel != nullptr) + { + auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); + if (message->tag("user-id") == currentUser->getUserId()) + { + auto badgesTag = message->tag("badges"); + if (badgesTag.isValid()) + { + auto parsedBadges = parseBadges(badgesTag.toString()); + twitchChannel->setMod(parsedBadges.contains("moderator")); + twitchChannel->setVIP(parsedBadges.contains("vip")); + twitchChannel->setStaff(parsedBadges.contains("staff")); + } + } + } + // This is for compatibility with older Chatterino versions. Twitch didn't use // to allow ZERO WIDTH JOINER unicode character, so Chatterino used ESCAPE_TAG // instead. // See https://github.com/Chatterino/chatterino2/issues/3384 and // https://mm2pl.github.io/emoji_rfc.pdf for more details - this->addMessage( - message, channelOrEmptyByTarget(message->target(), server), + message, chan, message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), server, false, message->isAction()); - auto chan = channelOrEmptyByTarget(message->target(), server); - if (chan->isEmpty()) - { - return; - } - if (message->tags().contains(u"pinned-chat-paid-amount"_s)) { auto ptr = TwitchMessageBuilder::buildHypeChatMessage(message); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 780d0998d23..9e31263197f 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -331,7 +331,7 @@ Split::Split(QWidget *parent) }, this->signalHolder_); - this->header_->updateModerationModeIcon(); + this->header_->updateIcons(); this->overlay_->hide(); this->setSizePolicy(QSizePolicy::MinimumExpanding, @@ -813,7 +813,7 @@ void Split::joinChannelInNewTab(ChannelPtr channel) void Split::refreshModerationMode() { - this->header_->updateModerationModeIcon(); + this->header_->updateIcons(); this->view_->queueLayout(); } @@ -865,7 +865,7 @@ void Split::setChannel(IndirectChannel newChannel) if (tc != nullptr) { this->usermodeChangedConnection_ = tc->userStateChanged.connect([this] { - this->header_->updateModerationModeIcon(); + this->header_->updateIcons(); this->header_->updateRoomModes(); }); @@ -881,19 +881,10 @@ void Split::setChannel(IndirectChannel newChannel) }); }); - this->header_->updateModerationModeIcon(); + this->header_->updateIcons(); this->header_->updateChannelText(); this->header_->updateRoomModes(); - if (newChannel.getType() == Channel::Type::Twitch) - { - this->header_->setChattersButtonVisible(true); - } - else - { - this->header_->setChattersButtonVisible(false); - } - this->channelSignalHolder_.managedConnect( this->channel_.get()->displayNameChanged, [this] { this->actionRequested.invoke(Action::RefreshTab); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 7e27ad16b17..b8b811ca4c4 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -230,7 +230,7 @@ SplitHeader::SplitHeader(Split *split) this->setMouseTracking(true); this->updateChannelText(); this->handleChannelChanged(); - this->updateModerationModeIcon(); + this->updateIcons(); // The lifetime of these signals are tied to the lifetime of the Split. // Since the SplitHeader is owned by the Split, they will always be destroyed @@ -247,7 +247,7 @@ SplitHeader::SplitHeader(Split *split) this->bSignals_.emplace_back( getIApp()->getAccounts()->twitch.currentUserChanged.connect([this] { - this->updateModerationModeIcon(); + this->updateIcons(); })); auto _ = [this](const auto &, const auto &) { @@ -755,11 +755,6 @@ void SplitHeader::setAddButtonVisible(bool value) this->addButton_->setVisible(value); } -void SplitHeader::setChattersButtonVisible(bool value) -{ - this->chattersButton_->setVisible(value); -} - void SplitHeader::updateChannelText() { auto indirectChannel = this->split_->getIndirectChannel(); @@ -838,26 +833,42 @@ void SplitHeader::updateChannelText() this->titleLabel_->setText(title.isEmpty() ? "" : title); } -void SplitHeader::updateModerationModeIcon() +void SplitHeader::updateIcons() { - auto moderationMode = this->split_->getModerationMode() && - !getSettings()->moderationActions.empty(); - - this->moderationButton_->setPixmap( - moderationMode ? getResources().buttons.modModeEnabled - : getResources().buttons.modModeDisabled); - auto channel = this->split_->getChannel(); auto *twitchChannel = dynamic_cast(channel.get()); - if (twitchChannel != nullptr && - (twitchChannel->hasModRights() || moderationMode)) + if (twitchChannel != nullptr) { - this->moderationButton_->show(); + auto moderationMode = this->split_->getModerationMode() && + !getSettings()->moderationActions.empty(); + + this->moderationButton_->setPixmap( + moderationMode ? getResources().buttons.modModeEnabled + : getResources().buttons.modModeDisabled); + + if (twitchChannel->hasModRights() || moderationMode) + { + this->moderationButton_->show(); + } + else + { + this->moderationButton_->hide(); + } + + if (twitchChannel->hasModRights()) + { + this->chattersButton_->show(); + } + else + { + this->chattersButton_->hide(); + } } else { this->moderationButton_->hide(); + this->chattersButton_->hide(); } } diff --git a/src/widgets/splits/SplitHeader.hpp b/src/widgets/splits/SplitHeader.hpp index 872ba8a8b7d..efae94849ab 100644 --- a/src/widgets/splits/SplitHeader.hpp +++ b/src/widgets/splits/SplitHeader.hpp @@ -29,10 +29,9 @@ class SplitHeader final : public BaseWidget explicit SplitHeader(Split *split); void setAddButtonVisible(bool value); - void setChattersButtonVisible(bool value); void updateChannelText(); - void updateModerationModeIcon(); + void updateIcons(); // Invoked when SplitHeader should update anything refering to a TwitchChannel's mode // has changed (e.g. sub mode toggled) void updateRoomModes(); From e56f7136a93b926a82af5ce0d70506ae4f499b22 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 10 Mar 2024 14:00:52 +0100 Subject: [PATCH 15/16] fix: truncate outgoing IRC messages to ensure we don't send more than 512 bytes (#5246) --- CHANGELOG.md | 1 + src/providers/irc/AbstractIrcServer.hpp | 2 +- src/providers/irc/IrcServer.cpp | 5 +++++ src/providers/irc/IrcServer.hpp | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4ea7dbeae..7183dd470ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ - 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) diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index cb8b17735ab..7a0e1ee639a 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -37,7 +37,7 @@ class AbstractIrcServer : public QObject void disconnect(); void sendMessage(const QString &channelName, const QString &message); - void sendRawMessage(const QString &rawMessage); + virtual void sendRawMessage(const QString &rawMessage); // channels ChannelPtr getOrAddChannel(const QString &dirtyChannelName); diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 0ae9849f5cb..8d54d79f7b0 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -371,6 +371,11 @@ void IrcServer::sendWhisper(const QString &target, const QString &message) } } +void IrcServer::sendRawMessage(const QString &rawMessage) +{ + AbstractIrcServer::sendRawMessage(rawMessage.left(510)); +} + bool IrcServer::hasEcho() const { return this->hasEcho_; diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp index a360493905d..199a9340ab2 100644 --- a/src/providers/irc/IrcServer.hpp +++ b/src/providers/irc/IrcServer.hpp @@ -25,6 +25,8 @@ class IrcServer : public AbstractIrcServer */ void sendWhisper(const QString &target, const QString &message); + void sendRawMessage(const QString &rawMessage) override; + // AbstractIrcServer interface protected: void initializeConnectionSignals(IrcConnection *connection, From e7508332ff1399a89424bfdc0997f979fd0c9acc Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 10 Mar 2024 14:27:08 +0100 Subject: [PATCH 16/16] refactor: Fonts (#5228) --- CHANGELOG.md | 1 + mocks/include/mocks/EmptyApplication.hpp | 5 +- src/Application.cpp | 6 +- src/Application.hpp | 2 +- src/singletons/Fonts.cpp | 165 ++++++++-------------- src/singletons/Fonts.hpp | 20 +-- src/singletons/Settings.hpp | 21 +++ src/singletons/WindowManager.cpp | 1 - src/widgets/BaseWindow.cpp | 3 +- src/widgets/Label.cpp | 17 ++- src/widgets/TooltipWidget.cpp | 11 +- src/widgets/dialogs/UserInfoPopup.cpp | 3 +- src/widgets/helper/ChannelView.cpp | 2 +- src/widgets/settingspages/GeneralPage.cpp | 5 +- tests/src/NotebookTab.cpp | 7 + tests/src/SplitInput.cpp | 6 +- 16 files changed, 130 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7183dd470ea..90c9d449be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,7 @@ - Dev: Move `clang-tidy` checker to its own CI job. (#4996) - Dev: Refactored the Image Uploader feature. (#4971) - Dev: Refactored the SplitOverlay code. (#5082) +- Dev: Refactored the Fonts code, making it less of a singleton. (#5228) - Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096, #5144) - Dev: Refactored emotes out of TwitchIrcServer. (#5120, #5146) - Dev: Refactored the ChatterinoBadges structure, making it less of a singleton. (#5103) diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index e70be2688c6..5d5ea1064ea 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -5,6 +5,8 @@ #include "singletons/Paths.hpp" #include "singletons/Updates.hpp" +#include + namespace chatterino::mock { class EmptyApplication : public IApplication @@ -235,7 +237,8 @@ class EmptyApplication : public IApplication return nullptr; } -private: +protected: + QTemporaryDir settingsDir; Paths paths_; Args args_; Updates updates_; diff --git a/src/Application.cpp b/src/Application.cpp index bede3f43388..e412fe8f4cf 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -118,7 +118,7 @@ Application::Application(Settings &_settings, const Paths &paths, : paths_(paths) , args_(_args) , themes(&this->emplace()) - , fonts(&this->emplace()) + , fonts(new Fonts(_settings)) , emotes(&this->emplace()) , accounts(&this->emplace()) , hotkeys(&this->emplace()) @@ -170,6 +170,7 @@ void Application::fakeDtor() this->bttvEmotes.reset(); this->ffzEmotes.reset(); this->seventvEmotes.reset(); + this->fonts.reset(); } void Application::initialize(Settings &settings, const Paths &paths) @@ -335,8 +336,9 @@ Theme *Application::getThemes() Fonts *Application::getFonts() { assertInGuiThread(); + assert(this->fonts); - return this->fonts; + return this->fonts.get(); } IEmotes *Application::getEmotes() diff --git a/src/Application.hpp b/src/Application.hpp index eb79ffc534f..f846ff00b07 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -135,7 +135,7 @@ class Application : public IApplication private: Theme *const themes{}; - Fonts *const fonts{}; + std::unique_ptr fonts{}; Emotes *const emotes{}; AccountController *const accounts{}; HotkeyController *const hotkeys{}; diff --git a/src/singletons/Fonts.cpp b/src/singletons/Fonts.cpp index b9f54451bba..7afe081ec62 100644 --- a/src/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -8,119 +8,73 @@ #include #include -#ifdef Q_OS_WIN32 -# define DEFAULT_FONT_FAMILY "Segoe UI" -# define DEFAULT_FONT_SIZE 10 -#else -# ifdef Q_OS_MACOS -# define DEFAULT_FONT_FAMILY "Helvetica Neue" -# define DEFAULT_FONT_SIZE 12 -# else -# define DEFAULT_FONT_FAMILY "Arial" -# define DEFAULT_FONT_SIZE 11 -# endif -#endif - -namespace chatterino { namespace { - int getBoldness() - { + +using namespace chatterino; + +int getBoldness() +{ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // From qfont.cpp - // https://github.com/qt/qtbase/blob/589c6d066f84833a7c3dda1638037f4b2e91b7aa/src/gui/text/qfont.cpp#L143-L169 - static constexpr std::array, 9> legacyToOpenTypeMap{{ - {0, QFont::Thin}, - {12, QFont::ExtraLight}, - {25, QFont::Light}, - {50, QFont::Normal}, - {57, QFont::Medium}, - {63, QFont::DemiBold}, - {75, QFont::Bold}, - {81, QFont::ExtraBold}, - {87, QFont::Black}, - }}; - - const int target = getSettings()->boldScale.getValue(); - - int result = QFont::Medium; - int closestDist = INT_MAX; - - // Go through and find the closest mapped value - for (const auto [weightOld, weightNew] : legacyToOpenTypeMap) + // From qfont.cpp + // https://github.com/qt/qtbase/blob/589c6d066f84833a7c3dda1638037f4b2e91b7aa/src/gui/text/qfont.cpp#L143-L169 + static constexpr std::array, 9> legacyToOpenTypeMap{{ + {0, QFont::Thin}, + {12, QFont::ExtraLight}, + {25, QFont::Light}, + {50, QFont::Normal}, + {57, QFont::Medium}, + {63, QFont::DemiBold}, + {75, QFont::Bold}, + {81, QFont::ExtraBold}, + {87, QFont::Black}, + }}; + + const int target = getSettings()->boldScale.getValue(); + + int result = QFont::Medium; + int closestDist = INT_MAX; + + // Go through and find the closest mapped value + for (const auto [weightOld, weightNew] : legacyToOpenTypeMap) + { + const int dist = qAbs(weightOld - target); + if (dist < closestDist) + { + result = weightNew; + closestDist = dist; + } + else { - const int dist = qAbs(weightOld - target); - if (dist < closestDist) - { - result = weightNew; - closestDist = dist; - } - else - { - // Break early since following values will be further away - break; - } + // Break early since following values will be further away + break; } + } - return result; + return result; #else - return getSettings()->boldScale.getValue(); + return getSettings()->boldScale.getValue(); #endif - } +} } // namespace -Fonts *Fonts::instance = nullptr; +namespace chatterino { -Fonts::Fonts() - : chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) - , chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) +Fonts::Fonts(Settings &settings) { - Fonts::instance = this; - this->fontsByType_.resize(size_t(FontStyle::EndType)); -} -void Fonts::initialize(Settings &, const Paths &) -{ - this->chatFontFamily.connect( - [this]() { - assertInGuiThread(); - - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); - - this->chatFontSize.connect( - [this]() { - assertInGuiThread(); - - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); - -#ifdef CHATTERINO - getSettings()->boldScale.connect( - [this]() { - assertInGuiThread(); - - // REMOVED - getIApp()->getWindows()->incGeneration(); - - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); -#endif + this->fontChangedListener.setCB([this] { + assertInGuiThread(); + + for (auto &map : this->fontsByType_) + { + map.clear(); + } + this->fontChanged.invoke(); + }); + this->fontChangedListener.addSetting(settings.chatFontFamily); + this->fontChangedListener.addSetting(settings.chatFontSize); + this->fontChangedListener.addSetting(settings.boldScale); } QFont Fonts::getFont(FontStyle type, float scale) @@ -159,6 +113,8 @@ Fonts::FontData &Fonts::getOrCreateFontData(FontStyle type, float scale) Fonts::FontData Fonts::createFontData(FontStyle type, float scale) { + auto *settings = getSettings(); + // check if it's a chat (scale the setting) if (type >= FontStyle::ChatStart && type <= FontStyle::ChatEnd) { @@ -176,8 +132,8 @@ Fonts::FontData Fonts::createFontData(FontStyle type, float scale) QFont::Weight(getBoldness())}; auto data = sizeScale[type]; return FontData( - QFont(this->chatFontFamily.getValue(), - int(this->chatFontSize.getValue() * data.scale * scale), + QFont(settings->chatFontFamily.getValue(), + int(settings->chatFontSize.getValue() * data.scale * scale), data.weight, data.italic)); } @@ -205,9 +161,4 @@ Fonts::FontData Fonts::createFontData(FontStyle type, float scale) } } -Fonts *getFonts() -{ - return Fonts::instance; -} - } // namespace chatterino diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index 1f0704d9970..e6ea324a04a 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -1,15 +1,13 @@ #pragma once -#include "common/ChatterinoSetting.hpp" -#include "common/Singleton.hpp" +#include "pajlada/settings/settinglistener.hpp" #include #include -#include #include -#include #include +#include namespace chatterino { @@ -38,23 +36,17 @@ enum class FontStyle : uint8_t { ChatEnd = ChatVeryLarge, }; -class Fonts final : public Singleton +class Fonts final { public: - Fonts(); - - void initialize(Settings &settings, const Paths &paths) override; + explicit Fonts(Settings &settings); // font data gets set in createFontData(...) QFont getFont(FontStyle type, float scale); QFontMetrics getFontMetrics(FontStyle type, float scale); - QStringSetting chatFontFamily; - IntSetting chatFontSize; - pajlada::Signals::NoArgSignal fontChanged; - static Fonts *instance; private: struct FontData { @@ -85,8 +77,8 @@ class Fonts final : public Singleton FontData createFontData(FontStyle type, float scale); std::vector> fontsByType_; -}; -Fonts *getFonts(); + pajlada::SettingListener fontChangedListener; +}; } // namespace chatterino diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 3e0c693f228..660b3c143d6 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -25,6 +25,19 @@ using TimeoutButton = std::pair; namespace chatterino { +#ifdef Q_OS_WIN32 +# define DEFAULT_FONT_FAMILY "Segoe UI" +# define DEFAULT_FONT_SIZE 10 +#else +# ifdef Q_OS_MACOS +# define DEFAULT_FONT_FAMILY "Helvetica Neue" +# define DEFAULT_FONT_SIZE 12 +# else +# define DEFAULT_FONT_FAMILY "Arial" +# define DEFAULT_FONT_SIZE 11 +# endif +#endif + void _actuallyRegisterSetting( std::weak_ptr setting); @@ -134,6 +147,14 @@ class Settings // BoolSetting collapseLongMessages = // {"/appearance/messages/collapseLongMessages", false}; + QStringSetting chatFontFamily{ + "/appearance/currentFontFamily", + DEFAULT_FONT_FAMILY, + }; + IntSetting chatFontSize{ + "/appearance/currentFontSize", + DEFAULT_FONT_SIZE, + }; BoolSetting hideReplyContext = {"/appearance/hideReplyContext", false}; BoolSetting showReplyButton = {"/appearance/showReplyButton", false}; BoolSetting stripReplyMention = {"/appearance/stripReplyMention", true}; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 7115470624c..56bea50ae69 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -9,7 +9,6 @@ #include "providers/irc/IrcChannel2.hpp" #include "providers/irc/IrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Fonts.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index c34edf96ff2..c819c8f4a88 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -775,7 +775,8 @@ void BaseWindow::scaleChangedEvent(float scale) this->calcButtonsSizes(); #endif - this->setFont(getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); + this->setFont( + getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); } void BaseWindow::paintEvent(QPaintEvent *) diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index bcec1d28072..37bd9df3601 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -1,4 +1,6 @@ -#include "Label.hpp" +#include "widgets/Label.hpp" + +#include "Application.hpp" #include @@ -14,9 +16,10 @@ Label::Label(BaseWidget *parent, QString text, FontStyle style) , text_(std::move(text)) , fontStyle_(style) { - this->connections_.managedConnect(getFonts()->fontChanged, [this] { - this->updateSize(); - }); + this->connections_.managedConnect(getIApp()->getFonts()->fontChanged, + [this] { + this->updateSize(); + }); } const QString &Label::getText() const @@ -92,12 +95,12 @@ void Label::paintEvent(QPaintEvent *) 1.0; #endif - QFontMetrics metrics = getFonts()->getFontMetrics( + QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics( this->getFontStyle(), this->scale() * 96.f / std::max( 0.01F, static_cast(this->logicalDpiX() * deviceDpi))); - painter.setFont(getFonts()->getFont( + painter.setFont(getIApp()->getFonts()->getFont( this->getFontStyle(), this->scale() * 96.f / std::max( @@ -128,7 +131,7 @@ void Label::paintEvent(QPaintEvent *) void Label::updateSize() { QFontMetrics metrics = - getFonts()->getFontMetrics(this->fontStyle_, this->scale()); + getIApp()->getFonts()->getFontMetrics(this->fontStyle_, this->scale()); int width = metrics.horizontalAdvance(this->text_) + (2 * this->getOffset()); diff --git a/src/widgets/TooltipWidget.cpp b/src/widgets/TooltipWidget.cpp index 2e1ddf89e06..00f5a2bec71 100644 --- a/src/widgets/TooltipWidget.cpp +++ b/src/widgets/TooltipWidget.cpp @@ -47,9 +47,10 @@ TooltipWidget::TooltipWidget(BaseWidget *parent) this->setLayout(this->vLayout_); this->currentStyle_ = TooltipStyle::Vertical; - this->connections_.managedConnect(getFonts()->fontChanged, [this] { - this->updateFont(); - }); + this->connections_.managedConnect(getIApp()->getFonts()->fontChanged, + [this] { + this->updateFont(); + }); this->updateFont(); auto *windows = getIApp()->getWindows(); @@ -299,8 +300,8 @@ void TooltipWidget::scaleChangedEvent(float) void TooltipWidget::updateFont() { - this->setFont( - getFonts()->getFont(FontStyle::ChatMediumSmall, this->scale())); + this->setFont(getIApp()->getFonts()->getFont(FontStyle::ChatMediumSmall, + this->scale())); } void TooltipWidget::setWordWrap(bool wrap) diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 20d42459a62..953be229cde 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -572,7 +572,8 @@ void UserInfoPopup::themeChangedEvent() for (auto &&child : this->findChildren()) { - child->setFont(getFonts()->getFont(FontStyle::UiMedium, this->scale())); + child->setFont( + getIApp()->getFonts()->getFont(FontStyle::UiMedium, this->scale())); } } diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index e7920d8789d..0cac3913621 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -620,7 +620,7 @@ void ChannelView::scaleChangedEvent(float scale) 0.01, this->logicalDpiX() * this->devicePixelRatioF()); #endif this->goToBottom_->getLabel().setFont( - getFonts()->getFont(FontStyle::UiMedium, factor)); + getIApp()->getFonts()->getFont(FontStyle::UiMedium, factor)); } } diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 546bc70f370..50136dceef2 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -166,8 +166,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) } layout.addDropdown( - "Font", {"Segoe UI", "Arial", "Choose..."}, - getIApp()->getFonts()->chatFontFamily, + "Font", {"Segoe UI", "Arial", "Choose..."}, s.chatFontFamily, [](auto val) { return val; }, @@ -177,7 +176,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) true, "", true); layout.addDropdown( "Font size", {"9pt", "10pt", "12pt", "14pt", "16pt", "20pt"}, - getIApp()->getFonts()->chatFontSize, + s.chatFontSize, [](auto val) { return QString::number(val) + "pt"; }, diff --git a/tests/src/NotebookTab.cpp b/tests/src/NotebookTab.cpp index 56a14989890..2ac4903f47e 100644 --- a/tests/src/NotebookTab.cpp +++ b/tests/src/NotebookTab.cpp @@ -5,6 +5,7 @@ #include "gmock/gmock.h" #include "mocks/EmptyApplication.hpp" #include "singletons/Fonts.hpp" +#include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "widgets/Notebook.hpp" @@ -21,6 +22,11 @@ namespace { class MockApplication : mock::EmptyApplication { public: + MockApplication() + : settings(this->settingsDir.filePath("settings.json")) + , fonts(this->settings) + { + } Theme *getThemes() override { return &this->theme; @@ -36,6 +42,7 @@ class MockApplication : mock::EmptyApplication return &this->fonts; } + Settings settings; Theme theme; HotkeyController hotkeys; Fonts fonts; diff --git a/tests/src/SplitInput.cpp b/tests/src/SplitInput.cpp index 5d542f735dc..ed092f94baf 100644 --- a/tests/src/SplitInput.cpp +++ b/tests/src/SplitInput.cpp @@ -9,6 +9,7 @@ #include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" #include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "widgets/Notebook.hpp" @@ -28,7 +29,9 @@ class MockApplication : mock::EmptyApplication { public: MockApplication() - : windowManager(this->paths) + : settings(this->settingsDir.filePath("settings.json")) + , fonts(this->settings) + , windowManager(this->paths) { } Theme *getThemes() override @@ -66,6 +69,7 @@ class MockApplication : mock::EmptyApplication return &this->emotes; } + Settings settings; Theme theme; HotkeyController hotkeys; Fonts fonts;