From 1d4c6d5a9eff59c36b4a3b2c154baeb8d387064a Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Thu, 11 May 2023 16:05:27 +0000 Subject: [PATCH 01/83] Fixed crash when scrolling up really fast. (#4621) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f7defb8a..906448966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Bugfix: Domains starting with `http` are now parsed as links again. (#4598) - Bugfix: Fixed click effects on buttons not being antialiased. (#4473) - Bugfix: Fixed Ctrl+Backspace not working after Select All in chat search popup. (#4461) +- Bugfix: Fixed crash when scrolling up really fast. (#4621) - Dev: Added the ability to control the `followRedirect` mode for requests. (#4594) ## 2.4.3 diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 12194ad4c..3d031dfa1 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1362,7 +1362,8 @@ void ChannelView::wheelEvent(QWheelEvent *event) { float mouseMultiplier = getSettings()->mouseScrollMultiplier; - qreal desired = this->scrollBar_->getDesiredValue(); + // This ensures snapshot won't be indexed out of bounds when scrolling really fast + qreal desired = std::max(0, this->scrollBar_->getDesiredValue()); qreal delta = event->angleDelta().y() * qreal(1.5) * mouseMultiplier; auto &snapshot = this->getMessagesSnapshot(); From 8e87886ccc5cdfacc973157ee19e45d42ca5654b Mon Sep 17 00:00:00 2001 From: Felanbird <41973452+Felanbird@users.noreply.github.com> Date: Sat, 13 May 2023 06:13:42 -0400 Subject: [PATCH 02/83] Reduce the size of the update prompt (#4626) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/widgets/dialogs/UpdateDialog.cpp | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906448966..784fc5556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Bugfix: Fixed the menu warping on macOS on Qt6. (#4595) - Bugfix: Fixed link tooltips not showing unless the thumbnail setting was enabled. (#4597) - Bugfix: Domains starting with `http` are now parsed as links again. (#4598) +- Bugfix: Reduced the size of the update prompt to prevent it from going off the users screen. (#4626) - Bugfix: Fixed click effects on buttons not being antialiased. (#4473) - Bugfix: Fixed Ctrl+Backspace not working after Select All in chat search popup. (#4461) - Bugfix: Fixed crash when scrolling up really fast. (#4621) diff --git a/src/widgets/dialogs/UpdateDialog.cpp b/src/widgets/dialogs/UpdateDialog.cpp index dcce88d1e..888d35827 100644 --- a/src/widgets/dialogs/UpdateDialog.cpp +++ b/src/widgets/dialogs/UpdateDialog.cpp @@ -41,7 +41,7 @@ UpdateDialog::UpdateDialog() }); this->setScaleIndependantHeight(150); - this->setScaleIndependantWidth(500); + this->setScaleIndependantWidth(250); } void UpdateDialog::updateStatusChanged(Updates::Status status) @@ -51,18 +51,18 @@ void UpdateDialog::updateStatusChanged(Updates::Status status) switch (status) { case Updates::UpdateAvailable: { - this->ui_.label->setText( - (Updates::instance().isDowngrade() - ? QString( - "The version online (%1) seems to be lower than the " - "current (%2).\nEither a version was reverted or " - "you are running a newer build.\n\nDo you want to " - "download and install it?") - .arg(Updates::instance().getOnlineVersion(), - Updates::instance().getCurrentVersion()) - : QString("An update (%1) is available.\n\nDo you want to " - "download and install it?") - .arg(Updates::instance().getOnlineVersion()))); + this->ui_.label->setText(( + Updates::instance().isDowngrade() + ? QString( + "The version online (%1) seems to be\nlower than the " + "current (%2).\nEither a version was reverted or " + "you are\nrunning a newer build.\n\nDo you want to " + "download and install it?") + .arg(Updates::instance().getOnlineVersion(), + Updates::instance().getCurrentVersion()) + : QString("An update (%1) is available.\n\nDo you want to " + "download and install it?") + .arg(Updates::instance().getOnlineVersion()))); this->updateGeometry(); } break; From 29a146278ce0c5b6a4d0d05e8c80706d7e7fc569 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 13 May 2023 16:12:26 +0200 Subject: [PATCH 03/83] Release v2.4.4 (#4631) --- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 +- resources/com.chatterino.chatterino.appdata.xml | 3 +++ src/common/Version.hpp | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784fc5556..eb8bc822f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.4.4 + - Minor: Added a Send button in the input box so you can click to send a message. This is disabled by default and can be enabled with the "Show send message button" setting. (#4607) - Minor: Improved error messages when the updater fails a download. (#4594) - Minor: Added `/shield` and `/shieldoff` commands to toggle shield mode. (#4580) diff --git a/CMakeLists.txt b/CMakeLists.txt index ff354d8f7..560f6ba71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" ) -project(chatterino VERSION 2.4.3) +project(chatterino VERSION 2.4.4) option(BUILD_APP "Build Chatterino" ON) option(BUILD_TESTS "Build the tests for Chatterino" OFF) diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 450bbff94..33b7a362b 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -32,6 +32,9 @@ chatterino + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.4 + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.3 diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 33a37b66a..ab7764e1a 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -24,7 +24,7 @@ * - 2.4.0-alpha.2 * - 2.4.0-alpha **/ -#define CHATTERINO_VERSION "2.4.3" +#define CHATTERINO_VERSION "2.4.4" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" From 4fa2cc26c9bffd5a6c770380c8a8bceb7368edbf Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 14 May 2023 10:55:48 +0200 Subject: [PATCH 04/83] Document our pubsub usage & eventual eventsub usage (#4630) * pubsub <-> eventsub documentation & clarification * Update Get Chatters documentation --- src/providers/twitch/api/README.md | 63 ++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index b05be517e..173292ca6 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -164,13 +164,62 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-emotes Not used anywhere at the moment. -## TMI - -The TMI api is undocumented. - ### Get Chatters -**Undocumented** +URL: https://dev.twitch.tv/docs/api/reference/#get-chatters -- We use this in `widgets/splits/Split.cpp showViewerList` -- We use this in `providers/twitch/TwitchChannel.cpp refreshChatters` +Used for the viewer list for moderators/broadcasters. + +## PubSub + +### Whispers + +We listen to the `whispers.` PubSub topic to receive information about incoming whispers to the user + +No EventSub alternative available. + +### Chat Moderator Actions + +We listen to the `chat_moderator_actions..` PubSub topic to receive information about incoming moderator events in a channel. + +We listen to this topic in every channel the user is a moderator. + +No complete EventSub alternative available yet. Some functionality can be pieced together but it would not be zero cost, causing the `max_total_cost` of 10 to cause issues. + +- For showing bans & timeouts: `channel.ban`, but does not work with moderator token??? +- For showing unbans & untimeouts: `channel.unban`, but does not work with moderator token??? +- Clear/delete message: not in eventsub, and IRC doesn't tell us which mod performed the action +- Roomstate (slow(off), followers(off), r9k(off), emoteonly(off), subscribers(off)) => not in eventsub, and IRC doesn't tell us which mod performed the action +- VIP added => not in eventsub, but not critical +- VIP removed => not in eventsub, but not critical +- Moderator added => channel.moderator.add eventsub, but doesn't work with moderator token +- Moderator removed => channel.moderator.remove eventsub, but doesn't work with moderator token +- Raid started => channel.raid eventsub, but cost=1 for moderator token +- Unraid => not in eventsub +- Add permitted term => not in eventsub +- Delete permitted term => not in eventsub +- Add blocked term => not in eventsub +- Delete blocked term => not in eventsub +- Modified automod properties => not in eventsub +- Approve unban request => cannot read moderator message in eventsub +- Deny unban request => not in eventsub + +### AutoMod Queue + +We listen to the `automod-queue..` PubSub topic to receive information about incoming automod events in a channel. + +We listen to this topic in every channel the user is a moderator. + +No EventSub alternative available yet. + +### Channel Point Rewards + +We listen to the `community-points-channel-v1.` PubSub topic to receive information about incoming channel points redemptions in a channel. + +The EventSub alternative requires broadcaster auth, which is not a feasible alternative. + +### Low Trust Users + +We want to listen to the `low-trust-users` PubSub topic to receive information about messages from users who are marked as low-trust. + +There is no EventSub alternative available yet. From ce47d27d4115e139e01dae6aacbf89b99757609c Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 16 May 2023 17:28:20 +0200 Subject: [PATCH 05/83] Refactor/Cleanup NetworkRequest and Related Code (#4633) Cleanup unused code (Twitch v5) Add json methods that simplify sending JSON. This also sets the Accepts header, since here, when JSON is sent, only JSON is accepted as a response (this is only done in Helix). Clarify helix request creations Cleaned some clang-tidy suggestions in Network{Request,Private} --- src/common/NetworkPrivate.cpp | 104 ++++++++++--------- src/common/NetworkPrivate.hpp | 2 +- src/common/NetworkRequest.cpp | 66 ++++++------ src/common/NetworkRequest.hpp | 18 ++-- src/providers/twitch/api/Helix.cpp | 156 ++++++++++++++--------------- src/providers/twitch/api/Helix.hpp | 8 +- 6 files changed, 184 insertions(+), 170 deletions(-) diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index 44cb87102..f0a74ed5a 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -72,12 +72,12 @@ void writeToCache(const std::shared_ptr &data, } } -void loadUncached(const std::shared_ptr &data) +void loadUncached(std::shared_ptr &&data) { DebugCount::increase("http request started"); NetworkRequester requester; - NetworkWorker *worker = new NetworkWorker; + auto *worker = new NetworkWorker; worker->moveToThread(&NetworkManager::workerThread); @@ -89,7 +89,7 @@ void loadUncached(const std::shared_ptr &data) data->timer_->start(data->timeoutMS_); } - auto reply = [&]() -> QNetworkReply * { + auto *reply = [&]() -> QNetworkReply * { switch (data->requestType_) { case NetworkRequestType::Get: @@ -245,12 +245,16 @@ void loadUncached(const std::shared_ptr &data) if (data->onSuccess_) { if (data->executeConcurrently_) + { QtConcurrent::run([onSuccess = std::move(data->onSuccess_), result = std::move(result)] { onSuccess(result); }); + } else + { data->onSuccess_(result); + } } // log("finished {}", data->request_.url().toString()); @@ -276,11 +280,15 @@ void loadUncached(const std::shared_ptr &data) if (data->finally_) { if (data->executeConcurrently_) + { QtConcurrent::run([finally = std::move(data->finally_)] { finally(); }); + } else + { data->finally_(); + } } }; @@ -316,87 +324,87 @@ void loadUncached(const std::shared_ptr &data) } // First tried to load cached, then uncached. -void loadCached(const std::shared_ptr &data) +void loadCached(std::shared_ptr &&data) { QFile cachedFile(getPaths()->cacheDirectory() + "/" + data->getHash()); if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly)) { // File didn't exist OR File could not be opened - loadUncached(data); + loadUncached(std::move(data)); return; } - else - { - // XXX: check if bytes is empty? - QByteArray bytes = cachedFile.readAll(); - NetworkResult result(bytes, 200); - qCDebug(chatterinoHTTP) - << QString("%1 [CACHED] 200 %2") - .arg(networkRequestTypes.at(int(data->requestType_)), - data->request_.url().toString()); - if (data->onSuccess_) + // XXX: check if bytes is empty? + QByteArray bytes = cachedFile.readAll(); + NetworkResult result(bytes, 200); + + qCDebug(chatterinoHTTP) + << QString("%1 [CACHED] 200 %2") + .arg(networkRequestTypes.at(int(data->requestType_)), + data->request_.url().toString()); + if (data->onSuccess_) + { + if (data->executeConcurrently_ || isGuiThread()) { - if (data->executeConcurrently_ || isGuiThread()) + // XXX: If outcome is Failure, we should invalidate the cache file + // somehow/somewhere + /*auto outcome =*/ + if (data->hasCaller_ && !data->caller_.get()) { - // XXX: If outcome is Failure, we should invalidate the cache file - // somehow/somewhere - /*auto outcome =*/ + return; + } + data->onSuccess_(result); + } + else + { + postToThread([data, result]() { if (data->hasCaller_ && !data->caller_.get()) { return; } + data->onSuccess_(result); - } - else - { - postToThread([data, result]() { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - data->onSuccess_(result); - }); - } + }); } + } - if (data->finally_) + if (data->finally_) + { + if (data->executeConcurrently_ || isGuiThread()) { - if (data->executeConcurrently_ || isGuiThread()) + if (data->hasCaller_ && !data->caller_.get()) { + return; + } + + data->finally_(); + } + else + { + postToThread([data]() { if (data->hasCaller_ && !data->caller_.get()) { return; } data->finally_(); - } - else - { - postToThread([data]() { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - data->finally_(); - }); - } + }); } } } -void load(const std::shared_ptr &data) +void load(std::shared_ptr &&data) { if (data->cache_) { - QtConcurrent::run(loadCached, data); + QtConcurrent::run([data = std::move(data)]() mutable { + loadCached(std::move(data)); + }); } else { - loadUncached(data); + loadUncached(std::move(data)); } } diff --git a/src/common/NetworkPrivate.hpp b/src/common/NetworkPrivate.hpp index 03d4a705e..3fe841bc2 100644 --- a/src/common/NetworkPrivate.hpp +++ b/src/common/NetworkPrivate.hpp @@ -68,6 +68,6 @@ private: QString hash_; }; -void load(const std::shared_ptr &data); +void load(std::shared_ptr &&data); } // namespace chatterino diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index cfe0cd177..d856faf1e 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -1,14 +1,8 @@ #include "common/NetworkRequest.hpp" #include "common/NetworkPrivate.hpp" -#include "common/Outcome.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include "singletons/Paths.hpp" -#include "util/DebugCount.hpp" -#include "util/PostToThread.hpp" #include #include @@ -28,7 +22,7 @@ NetworkRequest::NetworkRequest(const std::string &url, this->initializeDefaultValues(); } -NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType) +NetworkRequest::NetworkRequest(const QUrl &url, NetworkRequestType requestType) : data(new NetworkData) { this->data->request_.setUrl(url); @@ -37,10 +31,7 @@ NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType) this->initializeDefaultValues(); } -NetworkRequest::~NetworkRequest() -{ - //assert(!this->data || this->executed_); -} +NetworkRequest::~NetworkRequest() = default; NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) && { @@ -63,25 +54,25 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) && NetworkRequest NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) && { - this->data->onReplyCreated_ = cb; + this->data->onReplyCreated_ = std::move(cb); return std::move(*this); } NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) && { - this->data->onError_ = cb; + this->data->onError_ = std::move(cb); return std::move(*this); } NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) && { - this->data->onSuccess_ = cb; + this->data->onSuccess_ = std::move(cb); return std::move(*this); } NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) && { - this->data->finally_ = cb; + this->data->finally_ = std::move(cb); return std::move(*this); } @@ -106,6 +97,13 @@ NetworkRequest NetworkRequest::header(const char *headerName, return std::move(*this); } +NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header, + const QVariant &value) && +{ + this->data->request_.setHeader(header, value); + return std::move(*this); +} + NetworkRequest NetworkRequest::headerList( const std::vector> &headers) && { @@ -129,20 +127,6 @@ NetworkRequest NetworkRequest::concurrent() && return std::move(*this); } -NetworkRequest NetworkRequest::authorizeTwitchV5(const QString &clientID, - const QString &oauthToken) && -{ - // TODO: make two overloads, with and without oauth token - auto tmp = std::move(*this) - .header("Client-ID", clientID) - .header("Accept", "application/vnd.twitchtv.v5+json"); - - if (!oauthToken.isEmpty()) - return std::move(tmp).header("Authorization", "OAuth " + oauthToken); - else - return tmp; -} - NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) && { payload->setParent(this->data->lifetimeManager_); @@ -207,10 +191,28 @@ void NetworkRequest::initializeDefaultValues() this->data->request_.setRawHeader("User-Agent", userAgent); } -// Helper creator functions -NetworkRequest NetworkRequest::twitchRequest(QUrl url) +NetworkRequest NetworkRequest::json(const QJsonArray &root) && { - return NetworkRequest(url).authorizeTwitchV5(getDefaultClientID()); + return std::move(*this).json(QJsonDocument(root)); +} + +NetworkRequest NetworkRequest::json(const QJsonObject &root) && +{ + return std::move(*this).json(QJsonDocument(root)); +} + +NetworkRequest NetworkRequest::json(const QJsonDocument &document) && +{ + return std::move(*this).json(document.toJson(QJsonDocument::Compact)); +} + +NetworkRequest NetworkRequest::json(const QByteArray &payload) && +{ + return std::move(*this) + .payload(payload) + .header(QNetworkRequest::ContentTypeHeader, "application/json") + .header(QNetworkRequest::ContentLengthHeader, payload.length()) + .header("Accept", "application/json"); } } // namespace chatterino diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp index 85f34782a..9d55cf1b7 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/NetworkRequest.hpp @@ -6,6 +6,10 @@ #include +class QJsonArray; +class QJsonObject; +class QJsonDocument; + namespace chatterino { struct NetworkData; @@ -24,8 +28,8 @@ public: explicit NetworkRequest( const std::string &url, NetworkRequestType requestType = NetworkRequestType::Get); - explicit NetworkRequest( - QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); + explicit NetworkRequest(const QUrl &url, NetworkRequestType requestType = + NetworkRequestType::Get); // Enable move NetworkRequest(NetworkRequest &&other) = default; @@ -54,23 +58,25 @@ public: NetworkRequest header(const char *headerName, const char *value) &&; NetworkRequest header(const char *headerName, const QByteArray &value) &&; NetworkRequest header(const char *headerName, const QString &value) &&; + NetworkRequest header(QNetworkRequest::KnownHeaders header, + const QVariant &value) &&; NetworkRequest headerList( const std::vector> &headers) &&; NetworkRequest timeout(int ms) &&; NetworkRequest concurrent() &&; - NetworkRequest authorizeTwitchV5(const QString &clientID, - const QString &oauthToken = QString()) &&; NetworkRequest multiPart(QHttpMultiPart *payload) &&; /** * This will change `RedirectPolicyAttribute`. * `QNetworkRequest`'s defaults are used by default (Qt 5: no-follow, Qt 6: follow). */ NetworkRequest followRedirects(bool on) &&; + NetworkRequest json(const QJsonObject &root) &&; + NetworkRequest json(const QJsonArray &root) &&; + NetworkRequest json(const QJsonDocument &document) &&; + NetworkRequest json(const QByteArray &payload) &&; void execute(); - static NetworkRequest twitchRequest(QUrl url); - private: void initializeDefaultValues(); }; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 4dc4f1222..662484c98 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -52,7 +52,7 @@ void Helix::fetchUsers(QStringList userIds, QStringList userLogins, } // TODO: set on success and on error - this->makeRequest("users", urlQuery) + this->makeGet("users", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -142,7 +142,7 @@ void Helix::fetchUsersFollows( } // TODO: set on success and on error - this->makeRequest("users/follows", urlQuery) + this->makeGet("users/follows", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); if (root.empty()) @@ -186,7 +186,7 @@ void Helix::fetchStreams( } // TODO: set on success and on error - this->makeRequest("streams", urlQuery) + this->makeGet("streams", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -279,7 +279,7 @@ void Helix::fetchGames(QStringList gameIds, QStringList gameNames, } // TODO: set on success and on error - this->makeRequest("games", urlQuery) + this->makeGet("games", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -315,7 +315,7 @@ void Helix::searchGames(QString gameName, QUrlQuery urlQuery; urlQuery.addQueryItem("query", gameName); - this->makeRequest("search/categories", urlQuery) + this->makeGet("search/categories", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -372,8 +372,7 @@ void Helix::createClip(QString channelId, QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", channelId); - this->makeRequest("clips", urlQuery) - .type(NetworkRequestType::Post) + this->makePost("clips", urlQuery) .header("Content-Type", "application/json") .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); @@ -425,7 +424,7 @@ void Helix::getChannel(QString broadcasterId, QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("channels", urlQuery) + this->makeGet("channels", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -460,10 +459,8 @@ void Helix::createStreamMarker( } payload.insert("user_id", QJsonValue(broadcasterId)); - this->makeRequest("streams/markers", QUrlQuery()) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePost("streams/markers", QUrlQuery()) + .json(payload) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -515,7 +512,7 @@ void Helix::loadBlocks(QString userId, urlQuery.addQueryItem("broadcaster_id", userId); urlQuery.addQueryItem("first", "100"); - this->makeRequest("users/blocks", urlQuery) + this->makeGet("users/blocks", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -551,8 +548,7 @@ void Helix::blockUser(QString targetUserId, QUrlQuery urlQuery; urlQuery.addQueryItem("target_user_id", targetUserId); - this->makeRequest("users/blocks", urlQuery) - .type(NetworkRequestType::Put) + this->makePut("users/blocks", urlQuery) .onSuccess([successCallback](auto /*result*/) -> Outcome { successCallback(); return Success; @@ -571,8 +567,7 @@ void Helix::unblockUser(QString targetUserId, QUrlQuery urlQuery; urlQuery.addQueryItem("target_user_id", targetUserId); - this->makeRequest("users/blocks", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("users/blocks", urlQuery) .onSuccess([successCallback](auto /*result*/) -> Outcome { successCallback(); return Success; @@ -590,7 +585,6 @@ void Helix::updateChannel(QString broadcasterId, QString gameId, HelixFailureCallback failureCallback) { QUrlQuery urlQuery; - auto data = QJsonDocument(); auto obj = QJsonObject(); if (!gameId.isEmpty()) { @@ -611,12 +605,9 @@ void Helix::updateChannel(QString broadcasterId, QString gameId, return; } - data.setObject(obj); urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("channels", urlQuery) - .type(NetworkRequestType::Patch) - .header("Content-Type", "application/json") - .payload(data.toJson()) + this->makePatch("channels", urlQuery) + .json(obj) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { successCallback(result); return Success; @@ -638,10 +629,8 @@ void Helix::manageAutoModMessages( payload.insert("msg_id", msgID); payload.insert("action", action); - this->makeRequest("moderation/automod/message", QUrlQuery()) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePost("moderation/automod/message", QUrlQuery()) + .json(payload) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { successCallback(); return Success; @@ -697,7 +686,7 @@ void Helix::getCheermotes( urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("bits/cheermotes", urlQuery) + this->makeGet("bits/cheermotes", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto root = result.parseJson(); auto data = root.value("data"); @@ -735,7 +724,7 @@ void Helix::getEmoteSetData(QString emoteSetId, urlQuery.addQueryItem("emote_set_id", emoteSetId); - this->makeRequest("chat/emotes/set", urlQuery) + this->makeGet("chat/emotes/set", urlQuery) .onSuccess([successCallback, failureCallback, emoteSetId](auto result) -> Outcome { QJsonObject root = result.parseJson(); @@ -767,7 +756,7 @@ void Helix::getChannelEmotes( QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("chat/emotes", urlQuery) + this->makeGet("chat/emotes", urlQuery) .onSuccess([successCallback, failureCallback](NetworkResult result) -> Outcome { QJsonObject root = result.parseJson(); @@ -807,10 +796,8 @@ void Helix::updateUserChatColor( payload.insert("user_id", QJsonValue(userID)); payload.insert("color", QJsonValue(color)); - this->makeRequest("chat/color", QUrlQuery()) - .type(NetworkRequestType::Put) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePut("chat/color", QUrlQuery()) + .json(payload) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto obj = result.parseJson(); if (result.status() != 204) @@ -887,8 +874,7 @@ void Helix::deleteChatMessages( urlQuery.addQueryItem("message_id", messageID); } - this->makeRequest("moderation/chat", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("moderation/chat", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -966,8 +952,7 @@ void Helix::addChannelModerator( urlQuery.addQueryItem("broadcaster_id", broadcasterID); urlQuery.addQueryItem("user_id", userID); - this->makeRequest("moderation/moderators", urlQuery) - .type(NetworkRequestType::Post) + this->makePost("moderation/moderators", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1055,8 +1040,7 @@ void Helix::removeChannelModerator( urlQuery.addQueryItem("broadcaster_id", broadcasterID); urlQuery.addQueryItem("user_id", userID); - this->makeRequest("moderation/moderators", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("moderation/moderators", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1142,10 +1126,8 @@ void Helix::sendChatAnnouncement( std::string{magic_enum::enum_name(color)}; body.insert("color", QString::fromStdString(colorStr).toLower()); - this->makeRequest("chat/announcements", urlQuery) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(body).toJson(QJsonDocument::Compact)) + this->makePost("chat/announcements", urlQuery) + .json(body) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1214,8 +1196,7 @@ void Helix::addChannelVIP( urlQuery.addQueryItem("broadcaster_id", broadcasterID); urlQuery.addQueryItem("user_id", userID); - this->makeRequest("channels/vips", urlQuery) - .type(NetworkRequestType::Post) + this->makePost("channels/vips", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1293,8 +1274,7 @@ void Helix::removeChannelVIP( urlQuery.addQueryItem("broadcaster_id", broadcasterID); urlQuery.addQueryItem("user_id", userID); - this->makeRequest("channels/vips", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("channels/vips", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1383,8 +1363,7 @@ void Helix::unbanUser( urlQuery.addQueryItem("moderator_id", moderatorID); urlQuery.addQueryItem("user_id", userID); - this->makeRequest("moderation/bans", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("moderation/bans", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1489,8 +1468,7 @@ void Helix::startRaid( urlQuery.addQueryItem("from_broadcaster_id", fromBroadcasterID); urlQuery.addQueryItem("to_broadcaster_id", toBroadcasterID); - this->makeRequest("raids", urlQuery) - .type(NetworkRequestType::Post) + this->makePost("raids", urlQuery) .onSuccess( [successCallback, failureCallback](auto /*result*/) -> Outcome { successCallback(); @@ -1570,8 +1548,7 @@ void Helix::cancelRaid( urlQuery.addQueryItem("broadcaster_id", broadcasterID); - this->makeRequest("raids", urlQuery) - .type(NetworkRequestType::Delete) + this->makeDelete("raids", urlQuery) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -1731,10 +1708,8 @@ void Helix::updateChatSettings( urlQuery.addQueryItem("broadcaster_id", broadcasterID); urlQuery.addQueryItem("moderator_id", moderatorID); - this->makeRequest("chat/settings", urlQuery) - .type(NetworkRequestType::Patch) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePatch("chat/settings", urlQuery) + .json(payload) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -1857,7 +1832,7 @@ void Helix::fetchChatters( urlQuery.addQueryItem("after", after); } - this->makeRequest("chat/chatters", urlQuery) + this->makeGet("chat/chatters", urlQuery) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -1966,7 +1941,7 @@ void Helix::fetchModerators( urlQuery.addQueryItem("after", after); } - this->makeRequest("moderation/moderators", urlQuery) + this->makeGet("moderation/moderators", urlQuery) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -2051,10 +2026,8 @@ void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID, payload["data"] = data; } - this->makeRequest("moderation/bans", urlQuery) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePost("moderation/bans", urlQuery) + .json(payload) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -2150,10 +2123,8 @@ void Helix::sendWhisper( QJsonObject payload; payload["message"] = message; - this->makeRequest("whispers", urlQuery) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePost("whispers", urlQuery) + .json(payload) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 204) { @@ -2295,8 +2266,7 @@ void Helix::getChannelVIPs( // as the mod list can go over 100 (I assume, I see no limit) urlQuery.addQueryItem("first", "100"); - this->makeRequest("channels/vips", urlQuery) - .type(NetworkRequestType::Get) + this->makeGet("channels/vips", urlQuery) .header("Content-Type", "application/json") .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) @@ -2383,10 +2353,8 @@ void Helix::startCommercial( payload.insert("broadcaster_id", QJsonValue(broadcasterID)); payload.insert("length", QJsonValue(length)); - this->makeRequest("channels/commercial", QUrlQuery()) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePost("channels/commercial", QUrlQuery()) + .json(payload) .onSuccess([successCallback, failureCallback](auto result) -> Outcome { auto obj = result.parseJson(); if (obj.isEmpty()) @@ -2476,7 +2444,7 @@ void Helix::getGlobalBadges( { using Error = HelixGetGlobalBadgesError; - this->makeRequest("chat/badges/global", QUrlQuery()) + this->makeGet("chat/badges/global", QUrlQuery()) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -2523,7 +2491,7 @@ void Helix::getChannelBadges( QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterID); - this->makeRequest("chat/badges", urlQuery) + this->makeGet("chat/badges", urlQuery) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -2575,10 +2543,8 @@ void Helix::updateShieldMode( QJsonObject payload; payload["is_active"] = isActive; - this->makeRequest("moderation/shield_mode", urlQuery) - .type(NetworkRequestType::Put) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) + this->makePut("moderation/shield_mode", urlQuery) + .json(payload) .onSuccess([successCallback](auto result) -> Outcome { if (result.status() != 200) { @@ -2631,7 +2597,8 @@ void Helix::updateShieldMode( .execute(); } -NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) +NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery, + NetworkRequestType type) { assert(!url.startsWith("/")); @@ -2655,13 +2622,38 @@ NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) fullUrl.setQuery(urlQuery); - return NetworkRequest(fullUrl) + return NetworkRequest(fullUrl, type) .timeout(5 * 1000) .header("Accept", "application/json") .header("Client-ID", this->clientId) .header("Authorization", "Bearer " + this->oauthToken); } +NetworkRequest Helix::makeGet(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Get); +} + +NetworkRequest Helix::makeDelete(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Delete); +} + +NetworkRequest Helix::makePost(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Post); +} + +NetworkRequest Helix::makePut(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Put); +} + +NetworkRequest Helix::makePatch(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Patch); +} + void Helix::update(QString clientId, QString oauthToken) { this->clientId = std::move(clientId); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 0492b0560..f6dcc8f02 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -1361,7 +1361,13 @@ protected: FailureCallback failureCallback); private: - NetworkRequest makeRequest(QString url, QUrlQuery urlQuery); + NetworkRequest makeRequest(const QString &url, const QUrlQuery &urlQuery, + NetworkRequestType type); + NetworkRequest makeGet(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makeDelete(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePost(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePut(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePatch(const QString &url, const QUrlQuery &urlQuery); QString clientId; QString oauthToken; From 347f216abfbaf4cf1acc78caf70463c78ca4e0b5 Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 17 May 2023 23:32:50 +0200 Subject: [PATCH 06/83] Add Command to Set Logging/Filter Rules at Runtime (#4637) Co-authored-by: pajlada --- CHANGELOG.md | 2 + src/CMakeLists.txt | 2 + .../commands/CommandController.cpp | 3 ++ .../commands/builtin/chatterino/Debugging.cpp | 45 +++++++++++++++++++ .../commands/builtin/chatterino/Debugging.hpp | 15 +++++++ 5 files changed, 67 insertions(+) create mode 100644 src/controllers/commands/builtin/chatterino/Debugging.cpp create mode 100644 src/controllers/commands/builtin/chatterino/Debugging.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8bc822f..85ec55c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +- Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) + ## 2.4.4 - Minor: Added a Send button in the input box so you can click to send a message. This is disabled by default and can be enabled with the "Show send message button" setting. (#4607) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0fa8d9073..fe4a44fa4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,6 +60,8 @@ set(SOURCE_FILES controllers/accounts/AccountModel.cpp controllers/accounts/AccountModel.hpp + controllers/commands/builtin/chatterino/Debugging.cpp + controllers/commands/builtin/chatterino/Debugging.hpp controllers/commands/builtin/twitch/ChatSettings.cpp controllers/commands/builtin/twitch/ChatSettings.hpp controllers/commands/builtin/twitch/ShieldMode.cpp diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index a68f0545f..3c7c4febd 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -7,6 +7,7 @@ #include "common/QLogging.hpp" #include "common/SignalVector.hpp" #include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/builtin/chatterino/Debugging.hpp" #include "controllers/commands/builtin/twitch/ChatSettings.hpp" #include "controllers/commands/builtin/twitch/ShieldMode.hpp" #include "controllers/commands/Command.hpp" @@ -3211,6 +3212,8 @@ void CommandController::initialize(Settings &, Paths &paths) this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shieldoff", &commands::shieldModeOff); + + this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules); } void CommandController::save() diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp new file mode 100644 index 000000000..7482d68ce --- /dev/null +++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp @@ -0,0 +1,45 @@ +#include "controllers/commands/builtin/chatterino/Debugging.hpp" + +#include "common/Channel.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" + +#include +#include + +namespace chatterino::commands { + +QString setLoggingRules(const CommandContext &ctx) +{ + if (ctx.words.size() < 2) + { + ctx.channel->addMessage(makeSystemMessage( + "Usage: /c2-set-logging-rules . To enable debug logging " + "for all categories from chatterino, use " + "'chatterino.*.debug=true'. For the format on the rules, see " + "https://doc.qt.io/qt-6/" + "qloggingcategory.html#configuring-categories")); + return {}; + } + + auto filterRules = ctx.words.mid(1).join('\n'); + + QLoggingCategory::setFilterRules(filterRules); + + auto message = + QStringLiteral("Updated filter rules to '%1'.").arg(filterRules); + + if (!qgetenv("QT_LOGGING_RULES").isEmpty()) + { + message += QStringLiteral( + " Warning: Logging rules were previously set by the " + "QT_LOGGING_RULES environment variable. This might cause " + "interference - see: " + "https://doc.qt.io/qt-6/qloggingcategory.html#setFilterRules"); + } + + ctx.channel->addMessage(makeSystemMessage(message)); + return {}; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/chatterino/Debugging.hpp b/src/controllers/commands/builtin/chatterino/Debugging.hpp new file mode 100644 index 000000000..8cd2330ba --- /dev/null +++ b/src/controllers/commands/builtin/chatterino/Debugging.hpp @@ -0,0 +1,15 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString setLoggingRules(const CommandContext &ctx); + +} // namespace chatterino::commands From 82dff89f3b20ed588de8b36df6a944b49e3bbc70 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Fri, 19 May 2023 10:30:30 +0000 Subject: [PATCH 07/83] Add 'joined channel' system message. (#4616) --- CHANGELOG.md | 1 + src/providers/twitch/IrcMessageHandler.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ec55c55..5af8bbfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Minor: Added a Send button in the input box so you can click to send a message. This is disabled by default and can be enabled with the "Show send message button" setting. (#4607) - Minor: Improved error messages when the updater fails a download. (#4594) - Minor: Added `/shield` and `/shieldoff` commands to toggle shield mode. (#4580) +- Minor: Added a message for when Chatterino joins a channel (#4616) - Bugfix: Fixed the menu warping on macOS on Qt6. (#4595) - Bugfix: Fixed link tooltips not showing unless the thumbnail setting was enabled. (#4597) - Bugfix: Domains starting with `http` are now parsed as links again. (#4598) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 43fa8d49c..ce2752bf9 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1145,9 +1145,12 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) return; } - if (message->nick() != - getApp()->accounts->twitch.getCurrent()->getUserName() && - getSettings()->showJoins.getValue()) + if (message->nick() == + getApp()->accounts->twitch.getCurrent()->getUserName()) + { + twitchChannel->addMessage(makeSystemMessage("joined channel")); + } + else if (getSettings()->showJoins.getValue()) { twitchChannel->addJoinedUser(message->nick()); } From 5d0bdc195e42863c5176d8ba3c0ecd2409d50805 Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 19 May 2023 14:26:51 +0200 Subject: [PATCH 08/83] Add the ability to select custom themes in the settings dialog (#4570) Themes are loaded from the Themes directory (under the Chatterino directory, so %APPDATA%/Chatterino2/Themes). Themes are json files (see the built in themes as an example). After importing a theme, you must restart Chatterino for it to show up in the settings --- CHANGELOG.md | 1 + src/singletons/Paths.cpp | 1 + src/singletons/Paths.hpp | 3 + src/singletons/Theme.cpp | 189 +++++++++++++++--- src/singletons/Theme.hpp | 43 +++- src/widgets/settingspages/GeneralPage.cpp | 14 +- src/widgets/settingspages/GeneralPageView.hpp | 55 +++++ 7 files changed, 277 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af8bbfab..1d131fe76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) +- Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) ## 2.4.4 diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index 79344ac72..f2fa55335 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -142,6 +142,7 @@ void Paths::initSubDirectories() this->miscDirectory = makePath("Misc"); this->twitchProfileAvatars = makePath("ProfileAvatars"); this->pluginsDirectory = makePath("Plugins"); + this->themesDirectory = makePath("Themes"); this->crashdumpDirectory = makePath("Crashes"); //QDir().mkdir(this->twitchProfileAvatars + "/twitch"); } diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index f20195fef..d7f00e19e 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -37,6 +37,9 @@ public: // Plugin files live here. /Plugins QString pluginsDirectory; + // Custom themes live here. /Themes + QString themesDirectory; + bool createFolder(const QString &folderPath); bool isPortable(); diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 1b4f40cd2..98510662b 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -3,17 +3,21 @@ #include "Application.hpp" #include "common/QLogging.hpp" +#include "singletons/Paths.hpp" #include "singletons/Resources.hpp" #include +#include #include #include -#include #include #include namespace { + +using namespace chatterino; + void parseInto(const QJsonObject &obj, const QLatin1String &key, QColor &color) { const auto &jsonValue = obj[key]; @@ -139,62 +143,197 @@ void parseColors(const QJsonObject &root, chatterino::Theme &theme) } #undef parseColor -QString getThemePath(const QString &name) +/** + * Load the given theme descriptor from its path + * + * Returns a JSON object containing theme data if the theme is valid, otherwise nullopt + * + * NOTE: No theme validation is done by this function + **/ +std::optional loadTheme(const ThemeDescriptor &theme) { - static QSet knownThemes = {"White", "Light", "Dark", "Black"}; - - if (knownThemes.contains(name)) + QFile file(theme.path); + if (!file.open(QFile::ReadOnly)) { - return QStringLiteral(":/themes/%1.json").arg(name); + qCWarning(chatterinoTheme) + << "Failed to open" << file.fileName() << "at" << theme.path; + return std::nullopt; } - return name; + + QJsonParseError error{}; + auto json = QJsonDocument::fromJson(file.readAll(), &error); + if (!json.isObject()) + { + qCWarning(chatterinoTheme) << "Failed to parse" << file.fileName() + << "error:" << error.errorString(); + return std::nullopt; + } + + // TODO: Validate JSON schema? + + return json.object(); } } // namespace namespace chatterino { +const std::vector Theme::builtInThemes{ + { + .key = "White", + .path = ":/themes/White.json", + .name = "White", + }, + { + .key = "Light", + .path = ":/themes/Light.json", + .name = "Light", + }, + { + .key = "Dark", + .path = ":/themes/Dark.json", + .name = "Dark", + }, + { + .key = "Black", + .path = ":/themes/Black.json", + .name = "Black", + }, +}; + +// Dark is our default & fallback theme +const ThemeDescriptor Theme::fallbackTheme = Theme::builtInThemes.at(2); + bool Theme::isLightTheme() const { return this->isLight_; } -Theme::Theme() +void Theme::initialize(Settings &settings, Paths &paths) { - this->update(); - - this->themeName.connectSimple( - [this](auto) { + this->themeName.connect( + [this](auto themeName) { + qCInfo(chatterinoTheme) << "Theme updated to" << themeName; this->update(); }, false); + + this->loadAvailableThemes(); + + this->update(); } void Theme::update() { - this->parse(); + auto oTheme = this->findThemeByKey(this->themeName); + + std::optional themeJSON; + if (!oTheme) + { + qCWarning(chatterinoTheme) + << "Theme" << this->themeName + << "not found, falling back to the fallback theme"; + + themeJSON = loadTheme(fallbackTheme); + } + else + { + const auto &theme = *oTheme; + + themeJSON = loadTheme(theme); + + if (!themeJSON) + { + qCWarning(chatterinoTheme) + << "Theme" << this->themeName + << "not valid, falling back to the fallback theme"; + + // Parsing the theme failed, fall back + themeJSON = loadTheme(fallbackTheme); + } + } + + if (!themeJSON) + { + qCWarning(chatterinoTheme) + << "Failed to load" << this->themeName << "or the fallback theme"; + return; + } + + this->parseFrom(*themeJSON); + this->updated.invoke(); } -void Theme::parse() +std::vector> Theme::availableThemes() const { - QFile file(getThemePath(this->themeName)); - if (!file.open(QFile::ReadOnly)) + std::vector> packagedThemes; + + for (const auto &theme : this->availableThemes_) { - qCWarning(chatterinoTheme) << "Failed to open" << file.fileName(); - return; + if (theme.custom) + { + auto p = std::make_pair( + QStringLiteral("Custom: %1").arg(theme.name), theme.key); + + packagedThemes.emplace_back(p); + } + else + { + auto p = std::make_pair(theme.name, theme.key); + + packagedThemes.emplace_back(p); + } } - QJsonParseError error{}; - auto json = QJsonDocument::fromJson(file.readAll(), &error); - if (json.isNull()) + return packagedThemes; +} + +void Theme::loadAvailableThemes() +{ + this->availableThemes_ = Theme::builtInThemes; + + auto dir = QDir(getPaths()->themesDirectory); + for (const auto &info : + dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { - qCWarning(chatterinoTheme) << "Failed to parse" << file.fileName() - << "error:" << error.errorString(); - return; + if (!info.isFile()) + { + continue; + } + + if (!info.fileName().endsWith(".json")) + { + continue; + } + + auto themeName = info.baseName(); + + auto themeDescriptor = ThemeDescriptor{ + info.fileName(), info.absoluteFilePath(), themeName, true}; + + auto theme = loadTheme(themeDescriptor); + if (!theme) + { + qCWarning(chatterinoTheme) << "Failed to parse theme at" << info; + continue; + } + + this->availableThemes_.emplace_back(std::move(themeDescriptor)); + } +} + +std::optional Theme::findThemeByKey(const QString &key) +{ + for (const auto &theme : this->availableThemes_) + { + if (theme.key == key) + { + return theme; + } } - this->parseFrom(json.object()); + return std::nullopt; } void Theme::parseFrom(const QJsonObject &root) diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 034bb799e..3a7e1c984 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -6,16 +6,40 @@ #include #include +#include #include +#include +#include + +#include +#include namespace chatterino { class WindowManager; +struct ThemeDescriptor { + QString key; + + // Path to the theme on disk + // Can be a Qt resource path + QString path; + + // Name of the theme + QString name; + + bool custom{}; +}; + class Theme final : public Singleton { public: - Theme(); + static const std::vector builtInThemes; + + // The built in theme that will be used if some theme parsing fails + static const ThemeDescriptor fallbackTheme; + + void initialize(Settings &settings, Paths &paths) final; bool isLightTheme() const; @@ -114,6 +138,11 @@ public: void normalizeColor(QColor &color) const; void update(); + /** + * Return a list of available themes + **/ + std::vector> availableThemes() const; + pajlada::Signals::NoArgSignal updated; QStringSetting themeName{"/appearance/theme/name", "Dark"}; @@ -121,7 +150,17 @@ public: private: bool isLight_ = false; - void parse(); + std::vector availableThemes_; + + /** + * Figure out which themes are available in the Themes directory + * + * NOTE: This is currently not built to be reloadable + **/ + void loadAvailableThemes(); + + std::optional findThemeByKey(const QString &key); + void parseFrom(const QJsonObject &root); pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index d8234e9fb..5bf6fe675 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -114,8 +114,18 @@ void GeneralPage::initLayout(GeneralPageView &layout) auto &s = *getSettings(); layout.addTitle("Interface"); - layout.addDropdown("Theme", {"White", "Light", "Dark", "Black"}, - getApp()->themes->themeName); + + layout.addDropdown( + "Theme", getApp()->themes->availableThemes(), + getApp()->themes->themeName, + [](const auto *combo, const auto &themeKey) { + return combo->findData(themeKey, Qt::UserRole); + }, + [](const auto &args) { + return args.combobox->itemData(args.index, Qt::UserRole).toString(); + }, + {}, Theme::fallbackTheme.name); + layout.addDropdown( "Font", {"Segoe UI", "Arial", "Choose..."}, getApp()->fonts->chatFontFamily, diff --git a/src/widgets/settingspages/GeneralPageView.hpp b/src/widgets/settingspages/GeneralPageView.hpp index 38a043fe8..101b0b7b9 100644 --- a/src/widgets/settingspages/GeneralPageView.hpp +++ b/src/widgets/settingspages/GeneralPageView.hpp @@ -14,6 +14,8 @@ #include #include +#include + class QScrollArea; namespace chatterino { @@ -192,6 +194,59 @@ public: return combo; } + + template + ComboBox *addDropdown( + const QString &text, + const std::vector> &items, + pajlada::Settings::Setting &setting, + std::function(ComboBox *, T)> getValue, + std::function setValue, QString toolTipText = {}, + const QString &defaultValueText = {}) + { + auto *combo = this->addDropdown(text, {}, std::move(toolTipText)); + + for (const auto &[text, userData] : items) + { + combo->addItem(text, userData); + } + + if (!defaultValueText.isEmpty()) + { + combo->setCurrentText(defaultValueText); + } + + setting.connect( + [getValue = std::move(getValue), combo](const T &value, auto) { + auto var = getValue(combo, value); + if (var.which() == 0) + { + const auto index = boost::get(var); + if (index >= 0) + { + combo->setCurrentIndex(index); + } + } + else + { + combo->setCurrentText(boost::get(var)); + combo->setEditText(boost::get(var)); + } + }, + this->managedConnections_); + + QObject::connect( + combo, QOverload::of(&QComboBox::currentIndexChanged), + [combo, &setting, + setValue = std::move(setValue)](const int newIndex) { + setting = setValue(DropdownArgs{combo->itemText(newIndex), + combo->currentIndex(), combo}); + getApp()->windows->forceLayoutChannelViews(); + }); + + return combo; + } + DescriptionLabel *addDescription(const QString &text); void addSeperator(); From e1a6c24cf37302628b18aef7dde8bbdc769ccfef Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 20 May 2023 12:54:50 +0200 Subject: [PATCH 09/83] Move mocks to their own interface (#4645) --- CMakeLists.txt | 4 + benchmarks/CMakeLists.txt | 1 + benchmarks/src/Highlights.cpp | 53 +-- benchmarks/src/main.cpp | 7 + mocks/.clang-format | 55 +++ mocks/CMakeLists.txt | 7 + mocks/include/mocks/EmptyApplication.hpp | 81 ++++ mocks/include/mocks/Helix.hpp | 396 +++++++++++++++++ .../src => mocks/include}/mocks/UserData.hpp | 0 tests/CMakeLists.txt | 1 + tests/src/HighlightController.cpp | 409 +----------------- tests/src/TwitchMessageBuilder.cpp | 51 +-- 12 files changed, 564 insertions(+), 501 deletions(-) create mode 100644 mocks/.clang-format create mode 100644 mocks/CMakeLists.txt create mode 100644 mocks/include/mocks/EmptyApplication.hpp create mode 100644 mocks/include/mocks/Helix.hpp rename {tests/src => mocks/include}/mocks/UserData.hpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 560f6ba71..0714104ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,6 +174,10 @@ include(cmake/resources/generate_resources.cmake) add_subdirectory(src) +if (BUILD_TESTS OR BUILD_BENCHMARKS) + add_subdirectory(mocks) +endif () + if (BUILD_TESTS) enable_testing() add_subdirectory(tests) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 344258516..84ac8aa19 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(${PROJECT_NAME} ${benchmark_SOURCES}) add_sanitizers(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-lib) +target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-mocks) target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark) diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp index c35a0847f..303cb6612 100644 --- a/benchmarks/src/Highlights.cpp +++ b/benchmarks/src/Highlights.cpp @@ -1,11 +1,12 @@ #include "Application.hpp" -#include "singletons/Settings.hpp" #include "common/Channel.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "messages/Message.hpp" #include "messages/SharedMessageBuilder.hpp" +#include "mocks/EmptyApplication.hpp" +#include "singletons/Settings.hpp" #include "util/Helpers.hpp" #include @@ -45,65 +46,17 @@ public: } }; -class MockApplication : IApplication +class MockApplication : mock::EmptyApplication { public: - Theme *getThemes() override - { - return nullptr; - } - Fonts *getFonts() override - { - return nullptr; - } - IEmotes *getEmotes() override - { - return nullptr; - } AccountController *getAccounts() override { return &this->accounts; } - HotkeyController *getHotkeys() override - { - return nullptr; - } - WindowManager *getWindows() override - { - return nullptr; - } - Toasts *getToasts() override - { - return nullptr; - } - CommandController *getCommands() override - { - return nullptr; - } - NotificationController *getNotifications() override - { - return nullptr; - } HighlightController *getHighlights() override { return &this->highlights; } - TwitchIrcServer *getTwitch() override - { - return nullptr; - } - ChatterinoBadges *getChatterinoBadges() override - { - return nullptr; - } - FfzBadges *getFfzBadges() override - { - return nullptr; - } - IUserDataController *getUserData() override - { - return nullptr; - } AccountController accounts; HighlightController highlights; diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index 501b3aa51..cc4491f22 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -1,13 +1,20 @@ +#include "singletons/Settings.hpp" + #include #include #include +using namespace chatterino; + int main(int argc, char **argv) { QApplication app(argc, argv); ::benchmark::Initialize(&argc, argv); + // Ensure settings are initialized before any tests are run + chatterino::Settings settings("/tmp/c2-empty-test"); + QtConcurrent::run([&app] { ::benchmark::RunSpecifiedBenchmarks(); diff --git a/mocks/.clang-format b/mocks/.clang-format new file mode 100644 index 000000000..7bae09f2c --- /dev/null +++ b/mocks/.clang-format @@ -0,0 +1,55 @@ +Language: Cpp + +AccessModifierOffset: -4 +AlignEscapedNewlinesLeft: true +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakBeforeMultilineStrings: false +BasedOnStyle: Google +BraceWrapping: + AfterClass: "true" + AfterControlStatement: "true" + AfterFunction: "true" + AfterNamespace: "false" + BeforeCatch: "true" + BeforeElse: "true" +BreakBeforeBraces: Custom +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +DerivePointerBinding: false +FixNamespaceComments: true +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +IndentPPDirectives: AfterHash +SortIncludes: CaseInsensitive +IncludeBlocks: Regroup +IncludeCategories: + # Project includes + - Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$' + Priority: 1 + # Third party library includes + - Regex: '<[[:alnum:].]+/[a-zA-Z0-9\._\/-]+>' + Priority: 3 + # Qt includes + - Regex: '^$' + Priority: 3 + CaseSensitive: true + # LibCommuni includes + - Regex: "^$" + Priority: 3 + # Misc libraries + - Regex: '^<[a-zA-Z_0-9]+\.h(pp)?>$' + Priority: 3 + # Standard library includes + - Regex: "^<[a-zA-Z_]+>$" + Priority: 4 +NamespaceIndentation: Inner +PointerBindsToType: false +SpacesBeforeTrailingComments: 2 +Standard: Auto +ReflowComments: false diff --git a/mocks/CMakeLists.txt b/mocks/CMakeLists.txt new file mode 100644 index 000000000..47abd0ef4 --- /dev/null +++ b/mocks/CMakeLists.txt @@ -0,0 +1,7 @@ +project(chatterino-mocks) + +add_library(chatterino-mocks INTERFACE) + +target_include_directories(chatterino-mocks INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +target_link_libraries(${PROJECT_NAME} INTERFACE gmock) diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp new file mode 100644 index 000000000..846fd0aaa --- /dev/null +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "Application.hpp" + +namespace chatterino::mock { + +class EmptyApplication : public IApplication +{ +public: + Theme *getThemes() override + { + return nullptr; + } + + Fonts *getFonts() override + { + return nullptr; + } + + IEmotes *getEmotes() override + { + return nullptr; + } + + AccountController *getAccounts() override + { + return nullptr; + } + + HotkeyController *getHotkeys() override + { + return nullptr; + } + + WindowManager *getWindows() override + { + return nullptr; + } + + Toasts *getToasts() override + { + return nullptr; + } + + CommandController *getCommands() override + { + return nullptr; + } + + NotificationController *getNotifications() override + { + return nullptr; + } + + HighlightController *getHighlights() override + { + return nullptr; + } + + TwitchIrcServer *getTwitch() override + { + return nullptr; + } + + ChatterinoBadges *getChatterinoBadges() override + { + return nullptr; + } + + FfzBadges *getFfzBadges() override + { + return nullptr; + } + + IUserDataController *getUserData() override + { + return nullptr; + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp new file mode 100644 index 000000000..792094fbf --- /dev/null +++ b/mocks/include/mocks/Helix.hpp @@ -0,0 +1,396 @@ +#pragma once + +#include "providers/twitch/api/Helix.hpp" + +#include +#include +#include + +#include + +namespace chatterino::mock { + +class Helix : public IHelix +{ +public: + virtual ~Helix() = default; + + MOCK_METHOD(void, fetchUsers, + (QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getUserByName, + (QString userName, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + MOCK_METHOD(void, getUserById, + (QString userId, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, fetchUsersFollows, + (QString fromId, QString toId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getUserFollowers, + (QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, fetchStreams, + (QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, getStreamById, + (QString userId, + (ResultCallback successCallback), + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, getStreamByName, + (QString userName, + (ResultCallback successCallback), + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, fetchGames, + (QStringList gameIds, QStringList gameNames, + (ResultCallback> successCallback), + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, searchGames, + (QString gameName, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getGameById, + (QString gameId, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, createClip, + (QString channelId, ResultCallback successCallback, + std::function failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, getChannel, + (QString broadcasterId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, createStreamMarker, + (QString broadcasterId, QString description, + ResultCallback successCallback, + std::function failureCallback), + (override)); + + MOCK_METHOD(void, loadBlocks, + (QString userId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, blockUser, + (QString targetUserId, std::function successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, unblockUser, + (QString targetUserId, std::function successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, updateChannel, + (QString broadcasterId, QString gameId, QString language, + QString title, + std::function successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, manageAutoModMessages, + (QString userID, QString msgID, QString action, + std::function successCallback, + std::function failureCallback), + (override)); + + MOCK_METHOD(void, getCheermotes, + (QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getEmoteSetData, + (QString emoteSetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getChannelEmotes, + (QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getGlobalBadges, + (ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, getChannelBadges, + (QString broadcasterID, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateUserChatColor, + (QString userID, QString color, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, deleteChatMessages, + (QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, addChannelModerator, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, removeChannelModerator, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, sendChatAnnouncement, + (QString broadcasterID, QString moderatorID, QString message, + HelixAnnouncementColor color, ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, addChannelVIP, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, removeChannelVIP, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, unbanUser, + (QString broadcasterID, QString moderatorID, QString userID, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( // /raid + void, startRaid, + (QString fromBroadcasterID, QString toBroadcasterId, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /raid + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( // /unraid + void, cancelRaid, + (QString broadcasterID, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /unraid + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateEmoteMode, + (QString broadcasterID, QString moderatorID, bool emoteMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateFollowerMode, + (QString broadcasterID, QString moderatorID, + boost::optional followerModeDuration, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateNonModeratorChatDelay, + (QString broadcasterID, QString moderatorID, + boost::optional nonModeratorChatDelayDuration, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateSlowMode, + (QString broadcasterID, QString moderatorID, + boost::optional slowModeWaitTime, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateSubscriberMode, + (QString broadcasterID, QString moderatorID, + bool subscriberMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateUniqueChatMode, + (QString broadcasterID, QString moderatorID, + bool uniqueChatMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + // update chat settings + + // /timeout, /ban + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, banUser, + (QString broadcasterID, QString moderatorID, QString userID, + boost::optional duration, QString reason, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /timeout, /ban + + // /w + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, sendWhisper, + (QString fromUserID, QString toUserID, QString message, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /w + + // getChatters + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getChatters, + (QString broadcasterID, QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); // getChatters + + // /vips + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getChannelVIPs, + (QString broadcasterID, + ResultCallback> successCallback, + (FailureCallback failureCallback)), + (override)); // /vips + + // /commercial + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, startCommercial, + (QString broadcasterID, int length, + ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); // /commercial + + // /mods + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getModerators, + (QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + (FailureCallback failureCallback)), + (override)); // /mods + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateShieldMode, + (QString broadcasterID, QString moderatorID, bool isActive, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + MOCK_METHOD(void, update, (QString clientId, QString oauthToken), + (override)); + +protected: + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateChatSettings, + (QString broadcasterID, QString moderatorID, QJsonObject json, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); +}; + +} // namespace chatterino::mock diff --git a/tests/src/mocks/UserData.hpp b/mocks/include/mocks/UserData.hpp similarity index 100% rename from tests/src/mocks/UserData.hpp rename to mocks/include/mocks/UserData.hpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a3cb5ef0e..4f4cd2292 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(${PROJECT_NAME} ${test_SOURCES}) add_sanitizers(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-lib) +target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-mocks) target_link_libraries(${PROJECT_NAME} PRIVATE gtest gmock) diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index f5609ed45..b2f89e7a5 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -5,6 +5,8 @@ #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "messages/MessageBuilder.hpp" // for MessageParseArgs +#include "mocks/EmptyApplication.hpp" +#include "mocks/Helix.hpp" #include "mocks/UserData.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchBadge.hpp" // for Badge @@ -24,61 +26,19 @@ using ::testing::Exactly; namespace { -class MockApplication : IApplication +class MockApplication : mock::EmptyApplication { public: - Theme *getThemes() override - { - return nullptr; - } - Fonts *getFonts() override - { - return nullptr; - } - IEmotes *getEmotes() override - { - return nullptr; - } AccountController *getAccounts() override { return &this->accounts; } - HotkeyController *getHotkeys() override - { - return nullptr; - } - WindowManager *getWindows() override - { - return nullptr; - } - Toasts *getToasts() override - { - return nullptr; - } - CommandController *getCommands() override - { - return nullptr; - } - NotificationController *getNotifications() override - { - return nullptr; - } + HighlightController *getHighlights() override { return &this->highlights; } - TwitchIrcServer *getTwitch() override - { - return nullptr; - } - ChatterinoBadges *getChatterinoBadges() override - { - return nullptr; - } - FfzBadges *getFfzBadges() override - { - return nullptr; - } + IUserDataController *getUserData() override { return &this->userData; @@ -92,361 +52,6 @@ public: } // namespace -class MockHelix : public IHelix -{ -public: - MOCK_METHOD(void, fetchUsers, - (QStringList userIds, QStringList userLogins, - ResultCallback> successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, getUserByName, - (QString userName, ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - MOCK_METHOD(void, getUserById, - (QString userId, ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, fetchUsersFollows, - (QString fromId, QString toId, - ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, getUserFollowers, - (QString userId, - ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, fetchStreams, - (QStringList userIds, QStringList userLogins, - ResultCallback> successCallback, - HelixFailureCallback failureCallback, - std::function finallyCallback), - (override)); - - MOCK_METHOD(void, getStreamById, - (QString userId, - (ResultCallback successCallback), - HelixFailureCallback failureCallback, - std::function finallyCallback), - (override)); - - MOCK_METHOD(void, getStreamByName, - (QString userName, - (ResultCallback successCallback), - HelixFailureCallback failureCallback, - std::function finallyCallback), - (override)); - - MOCK_METHOD(void, fetchGames, - (QStringList gameIds, QStringList gameNames, - (ResultCallback> successCallback), - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, searchGames, - (QString gameName, - ResultCallback> successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, getGameById, - (QString gameId, ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, createClip, - (QString channelId, ResultCallback successCallback, - std::function failureCallback, - std::function finallyCallback), - (override)); - - MOCK_METHOD(void, getChannel, - (QString broadcasterId, - ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, createStreamMarker, - (QString broadcasterId, QString description, - ResultCallback successCallback, - std::function failureCallback), - (override)); - - MOCK_METHOD(void, loadBlocks, - (QString userId, - ResultCallback> successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, blockUser, - (QString targetUserId, std::function successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, unblockUser, - (QString targetUserId, std::function successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, updateChannel, - (QString broadcasterId, QString gameId, QString language, - QString title, - std::function successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, manageAutoModMessages, - (QString userID, QString msgID, QString action, - std::function successCallback, - std::function failureCallback), - (override)); - - MOCK_METHOD(void, getCheermotes, - (QString broadcasterId, - ResultCallback> successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, getEmoteSetData, - (QString emoteSetId, - ResultCallback successCallback, - HelixFailureCallback failureCallback), - (override)); - - MOCK_METHOD(void, getChannelEmotes, - (QString broadcasterId, - ResultCallback> successCallback, - HelixFailureCallback failureCallback), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, getGlobalBadges, - (ResultCallback successCallback, - (FailureCallback failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, getChannelBadges, - (QString broadcasterID, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateUserChatColor, - (QString userID, QString color, - ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, deleteChatMessages, - (QString broadcasterID, QString moderatorID, QString messageID, - ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, addChannelModerator, - (QString broadcasterID, QString userID, - ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, removeChannelModerator, - (QString broadcasterID, QString userID, - ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, sendChatAnnouncement, - (QString broadcasterID, QString moderatorID, QString message, - HelixAnnouncementColor color, ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, addChannelVIP, - (QString broadcasterID, QString userID, - ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, removeChannelVIP, - (QString broadcasterID, QString userID, - ResultCallback<> successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, unbanUser, - (QString broadcasterID, QString moderatorID, QString userID, - ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( // /raid - void, startRaid, - (QString fromBroadcasterID, QString toBroadcasterId, - ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); // /raid - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( // /unraid - void, cancelRaid, - (QString broadcasterID, ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); // /unraid - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateEmoteMode, - (QString broadcasterID, QString moderatorID, bool emoteMode, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateFollowerMode, - (QString broadcasterID, QString moderatorID, - boost::optional followerModeDuration, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateNonModeratorChatDelay, - (QString broadcasterID, QString moderatorID, - boost::optional nonModeratorChatDelayDuration, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateSlowMode, - (QString broadcasterID, QString moderatorID, - boost::optional slowModeWaitTime, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateSubscriberMode, - (QString broadcasterID, QString moderatorID, - bool subscriberMode, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateUniqueChatMode, - (QString broadcasterID, QString moderatorID, - bool uniqueChatMode, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - // update chat settings - - // /timeout, /ban - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, banUser, - (QString broadcasterID, QString moderatorID, QString userID, - boost::optional duration, QString reason, - ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); // /timeout, /ban - - // /w - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, sendWhisper, - (QString fromUserID, QString toUserID, QString message, - ResultCallback<> successCallback, - (FailureCallback failureCallback)), - (override)); // /w - - // getChatters - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, getChatters, - (QString broadcasterID, QString moderatorID, int maxChattersToFetch, - ResultCallback successCallback, - (FailureCallback failureCallback)), - (override)); // getChatters - - // /vips - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, getChannelVIPs, - (QString broadcasterID, - ResultCallback> successCallback, - (FailureCallback failureCallback)), - (override)); // /vips - - // /commercial - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, startCommercial, - (QString broadcasterID, int length, - ResultCallback successCallback, - (FailureCallback failureCallback)), - (override)); // /commercial - - // /mods - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD( - void, getModerators, - (QString broadcasterID, int maxModeratorsToFetch, - ResultCallback> successCallback, - (FailureCallback failureCallback)), - (override)); // /mods - - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateShieldMode, - (QString broadcasterID, QString moderatorID, bool isActive, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); - - MOCK_METHOD(void, update, (QString clientId, QString oauthToken), - (override)); - -protected: - // The extra parenthesis around the failure callback is because its type contains a comma - MOCK_METHOD(void, updateChatSettings, - (QString broadcasterID, QString moderatorID, QJsonObject json, - ResultCallback successCallback, - (FailureCallback - failureCallback)), - (override)); -}; - static QString DEFAULT_SETTINGS = R"!( { "accounts": { @@ -582,7 +187,7 @@ protected: ASSERT_TRUE(settingsFile.flush()); settingsFile.close(); - this->mockHelix = new MockHelix; + this->mockHelix = new mock::Helix; initializeHelix(this->mockHelix); @@ -618,7 +223,7 @@ protected: std::unique_ptr controller; - MockHelix *mockHelix; + mock::Helix *mockHelix; }; TEST_F(HighlightControllerTest, A) diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index b1ce38eb9..e167a7773 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -3,6 +3,7 @@ #include "Application.hpp" #include "common/Channel.hpp" #include "messages/MessageBuilder.hpp" +#include "mocks/EmptyApplication.hpp" #include "mocks/UserData.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "singletons/Emotes.hpp" @@ -19,61 +20,13 @@ using namespace chatterino; namespace { -class MockApplication : IApplication +class MockApplication : mock::EmptyApplication { public: - Theme *getThemes() override - { - return nullptr; - } - Fonts *getFonts() override - { - return nullptr; - } IEmotes *getEmotes() override { return &this->emotes; } - AccountController *getAccounts() override - { - return nullptr; - } - HotkeyController *getHotkeys() override - { - return nullptr; - } - WindowManager *getWindows() override - { - return nullptr; - } - Toasts *getToasts() override - { - return nullptr; - } - CommandController *getCommands() override - { - return nullptr; - } - NotificationController *getNotifications() override - { - return nullptr; - } - HighlightController *getHighlights() override - { - return nullptr; - } - TwitchIrcServer *getTwitch() override - { - return nullptr; - } - ChatterinoBadges *getChatterinoBadges() override - { - return nullptr; - } - FfzBadges *getFfzBadges() override - { - return nullptr; - } IUserDataController *getUserData() override { return &this->userData; From 21d4b2cacc622d1e91bf97c5e260c5f3885d7da7 Mon Sep 17 00:00:00 2001 From: olafyang <44876184+olafyang@users.noreply.github.com> Date: Sat, 20 May 2023 18:32:06 +0200 Subject: [PATCH 10/83] add "/shoutout" command (#4638) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + mocks/include/mocks/Helix.hpp | 8 ++ src/CMakeLists.txt | 2 + .../commands/CommandController.cpp | 3 + .../commands/builtin/twitch/Shoutout.cpp | 112 ++++++++++++++++++ .../commands/builtin/twitch/Shoutout.hpp | 15 +++ src/providers/twitch/api/Helix.cpp | 101 ++++++++++++++++ src/providers/twitch/api/Helix.hpp | 24 ++++ src/providers/twitch/api/README.md | 8 ++ 9 files changed, 274 insertions(+) create mode 100644 src/controllers/commands/builtin/twitch/Shoutout.cpp create mode 100644 src/controllers/commands/builtin/twitch/Shoutout.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d131fe76..e6ca8d19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp index 792094fbf..e7fe4ef49 100644 --- a/mocks/include/mocks/Helix.hpp +++ b/mocks/include/mocks/Helix.hpp @@ -379,6 +379,14 @@ public: failureCallback)), (override)); + // /shoutout + MOCK_METHOD( + void, sendShoutout, + (QString fromBroadcasterID, QString toBroadcasterID, + QString moderatorID, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + MOCK_METHOD(void, update, (QString clientId, QString oauthToken), (override)); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fe4a44fa4..465a147f1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -66,6 +66,8 @@ set(SOURCE_FILES controllers/commands/builtin/twitch/ChatSettings.hpp controllers/commands/builtin/twitch/ShieldMode.cpp controllers/commands/builtin/twitch/ShieldMode.hpp + controllers/commands/builtin/twitch/Shoutout.cpp + controllers/commands/builtin/twitch/Shoutout.hpp controllers/commands/CommandContext.hpp controllers/commands/CommandController.cpp controllers/commands/CommandController.hpp diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 3c7c4febd..7be61286c 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -10,6 +10,7 @@ #include "controllers/commands/builtin/chatterino/Debugging.hpp" #include "controllers/commands/builtin/twitch/ChatSettings.hpp" #include "controllers/commands/builtin/twitch/ShieldMode.hpp" +#include "controllers/commands/builtin/twitch/Shoutout.hpp" #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandContext.hpp" #include "controllers/commands/CommandModel.hpp" @@ -3213,6 +3214,8 @@ void CommandController::initialize(Settings &, Paths &paths) this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shieldoff", &commands::shieldModeOff); + this->registerCommand("/shoutout", &commands::sendShoutout); + this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules); } diff --git a/src/controllers/commands/builtin/twitch/Shoutout.cpp b/src/controllers/commands/builtin/twitch/Shoutout.cpp new file mode 100644 index 000000000..4e3a7c7d6 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Shoutout.cpp @@ -0,0 +1,112 @@ +#include "controllers/commands/builtin/twitch/Shoutout.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace chatterino::commands { + +QString sendShoutout(const CommandContext &ctx) +{ + auto *twitchChannel = ctx.twitchChannel; + auto channel = ctx.channel; + auto words = &ctx.words; + + if (twitchChannel == nullptr) + { + channel->addMessage(makeSystemMessage( + "The /shoutout command only works in Twitch channels")); + return ""; + } + + auto currentUser = getApp()->accounts->twitch.getCurrent(); + if (currentUser->isAnon()) + { + channel->addMessage( + makeSystemMessage("You must be logged in to send shoutout")); + return ""; + } + + if (words->size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage: \"/shoutout \" - Sends a " + "shoutout to the specified twitch user")); + return ""; + } + + const auto target = words->at(1); + + using Error = HelixSendShoutoutError; + + getHelix()->getUserByName( + target, + [twitchChannel, channel, currentUser, &target](const auto targetUser) { + getHelix()->sendShoutout( + twitchChannel->roomId(), targetUser.id, + currentUser->getUserId(), + [channel, targetUser]() { + channel->addMessage(makeSystemMessage( + QString("Sent shoutout to %1").arg(targetUser.login))); + }, + [channel](auto error, auto message) { + QString errorMessage = "Failed to send shoutout - "; + + switch (error) + { + case Error::UserNotAuthorized: { + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. " + "Try again in a few seconds."; + } + break; + + case Error::UserIsBroadcaster: { + errorMessage += "The broadcaster may not give " + "themselves a Shoutout."; + } + break; + + case Error::BroadcasterNotLive: { + errorMessage += + "The broadcaster is not streaming live or " + "does not have one or more viewers."; + } + break; + + case Error::Unknown: { + errorMessage += message; + } + break; + } + + channel->addMessage(makeSystemMessage(errorMessage)); + }); + }, + [channel, target] { + // Equivalent error from IRC + channel->addMessage( + makeSystemMessage(QString("Invalid username: %1").arg(target))); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Shoutout.hpp b/src/controllers/commands/builtin/twitch/Shoutout.hpp new file mode 100644 index 000000000..ffeec03f8 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Shoutout.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString sendShoutout(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 662484c98..59329ae59 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -2597,6 +2597,107 @@ void Helix::updateShieldMode( .execute(); } +// https://dev.twitch.tv/docs/api/reference/#send-a-shoutout +void Helix::sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixSendShoutoutError; + + QUrlQuery urlQuery; + urlQuery.addQueryItem("from_broadcaster_id", fromBroadcasterID); + urlQuery.addQueryItem("to_broadcaster_id", toBroadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + this->makePost("chat/shoutouts", urlQuery) + .header("Content-Type", "application/json") + .onSuccess([successCallback](NetworkResult result) -> Outcome { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for sending shoutout was " + << result.status() << "but we expected it to be 204"; + } + + successCallback(); + return Success; + }) + .onError([failureCallback](NetworkResult result) -> void { + const auto obj = result.parseJson(); + auto message = obj["message"].toString(); + + switch (result.status()) + { + case 400: { + if (message.startsWith("The broadcaster may not give " + "themselves a Shoutout.", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserIsBroadcaster, message); + } + else if (message.startsWith( + "The broadcaster is not streaming live or " + "does not have one or more viewers.", + Qt::CaseInsensitive)) + { + failureCallback(Error::BroadcasterNotLive, message); + } + else + { + failureCallback(Error::UserNotAuthorized, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::UserNotAuthorized, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + case 500: { + // Helix returns 500 when user is not mod, + if (message.isEmpty()) + { + failureCallback(Error::Unknown, + "Twitch internal server error"); + } + else + { + failureCallback(Error::Unknown, message); + } + } + break; + + default: { + qCWarning(chatterinoTwitch) + << "Helix send shoutout, unhandled error data:" + << result.status() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + } + }) + .execute(); +} + NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery, NetworkRequestType type) { diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index f6dcc8f02..3a370a41e 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -637,6 +637,18 @@ enum class HelixListVIPsError { // /vips Forwarded, }; // /vips +enum class HelixSendShoutoutError { + Unknown, + // 400 + UserIsBroadcaster, + BroadcasterNotLive, + // 401 + UserNotAuthorized, + UserMissingScope, + + Ratelimited, +}; + struct HelixStartCommercialResponse { // Length of the triggered commercial int length; @@ -1013,6 +1025,12 @@ public: FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + virtual void sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + virtual void update(QString clientId, QString oauthToken) = 0; protected: @@ -1318,6 +1336,12 @@ public: FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + void sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + void update(QString clientId, QString oauthToken) final; static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 173292ca6..83bc45e87 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -170,6 +170,14 @@ URL: https://dev.twitch.tv/docs/api/reference/#get-chatters Used for the viewer list for moderators/broadcasters. +### Send Shoutout + +URL: https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + +Used in: + +- `controllers/commands/CommandController.cpp` to send Twitch native shoutout using "/shoutout " + ## PubSub ### Whispers From e9f300b765a79e9e8fd1b65065701b423ad87a27 Mon Sep 17 00:00:00 2001 From: olafyang <44876184+olafyang@users.noreply.github.com> Date: Sat, 20 May 2023 20:08:22 +0200 Subject: [PATCH 11/83] add 'olafyang' to contributors.txt (#4646) Co-authored-by: pajlada --- resources/contributors.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/contributors.txt b/resources/contributors.txt index 70b2d517e..2a8be7985 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -62,6 +62,7 @@ ScrubN | https://github.com/ScrubN | | Contributor Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor 2547techno | https://github.com/2547techno | :/avatars/techno.png | Contributor ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor +olafyang | https://github.com/olafyang | | Contributor # If you are a contributor add yourself above this line From 51f2c4d1c06eda88df82307dbb3df504f96a1dfc Mon Sep 17 00:00:00 2001 From: Daniel Sage <24928223+dnsge@users.noreply.github.com> Date: Sun, 21 May 2023 06:10:49 -0400 Subject: [PATCH 12/83] Add input completion test suite (#4644) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + benchmarks/src/main.cpp | 2 +- mocks/include/mocks/EmptyApplication.hpp | 2 +- src/Application.cpp | 5 + src/Application.hpp | 8 +- src/common/CompletionModel.cpp | 31 +- src/common/CompletionModel.hpp | 6 + src/providers/bttv/BttvEmotes.cpp | 9 +- src/providers/bttv/BttvEmotes.hpp | 1 + src/providers/emoji/Emojis.cpp | 14 +- src/providers/emoji/Emojis.hpp | 22 +- src/providers/ffz/FfzEmotes.cpp | 9 +- src/providers/ffz/FfzEmotes.hpp | 1 + src/providers/seventv/SeventvEmotes.cpp | 10 +- src/providers/seventv/SeventvEmotes.hpp | 1 + src/providers/twitch/TwitchIrcServer.hpp | 22 +- src/providers/twitch/api/README.md | 2 +- src/singletons/Emotes.hpp | 6 + src/widgets/splits/InputCompletionPopup.cpp | 68 ++-- src/widgets/splits/InputCompletionPopup.hpp | 17 + tests/CMakeLists.txt | 1 + tests/src/HighlightController.cpp | 1 - tests/src/InputCompletion.cpp | 336 ++++++++++++++++++++ tests/src/TwitchMessageBuilder.cpp | 2 +- 24 files changed, 514 insertions(+), 63 deletions(-) create mode 100644 tests/src/InputCompletion.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ca8d19e..bd82de959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) +- Dev: Added test cases for emote and tab completion. (#4644) ## 2.4.4 diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index cc4491f22..5f4c967f8 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -13,7 +13,7 @@ int main(int argc, char **argv) ::benchmark::Initialize(&argc, argv); // Ensure settings are initialized before any tests are run - chatterino::Settings settings("/tmp/c2-empty-test"); + chatterino::Settings settings("/tmp/c2-empty-mock"); QtConcurrent::run([&app] { ::benchmark::RunSpecifiedBenchmarks(); diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 846fd0aaa..4dabc4ffa 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -57,7 +57,7 @@ public: return nullptr; } - TwitchIrcServer *getTwitch() override + ITwitchIrcServer *getTwitch() override { return nullptr; } diff --git a/src/Application.cpp b/src/Application.cpp index b794a7966..248cb0d86 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -245,6 +245,11 @@ IUserDataController *Application::getUserData() return this->userData; } +ITwitchIrcServer *Application::getTwitch() +{ + return this->twitch; +} + void Application::save() { for (auto &singleton : this->singletons_) diff --git a/src/Application.hpp b/src/Application.hpp index 7c5525505..27f1c2f60 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -10,6 +10,7 @@ namespace chatterino { class TwitchIrcServer; +class ITwitchIrcServer; class PubSub; class CommandController; @@ -55,7 +56,7 @@ public: virtual CommandController *getCommands() = 0; virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; - virtual TwitchIrcServer *getTwitch() = 0; + virtual ITwitchIrcServer *getTwitch() = 0; virtual ChatterinoBadges *getChatterinoBadges() = 0; virtual FfzBadges *getFfzBadges() = 0; virtual IUserDataController *getUserData() = 0; @@ -141,10 +142,7 @@ public: { return this->highlights; } - TwitchIrcServer *getTwitch() override - { - return this->twitch; - } + ITwitchIrcServer *getTwitch() override; ChatterinoBadges *getChatterinoBadges() override { return this->chatterinoBadges; diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 9b123aa4c..61f1cc249 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -92,6 +92,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) return; } + auto *app = getIApp(); // Twitch channel auto *tc = dynamic_cast(&this->channel_); @@ -130,7 +131,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } }; - if (auto account = getApp()->accounts->twitch.getCurrent()) + if (auto account = app->getAccounts()->twitch.getCurrent()) { // Twitch Emotes available globally for (const auto &emote : account->accessEmotes()->emotes) @@ -153,18 +154,18 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) // 7TV Global for (const auto &emote : - *getApp()->twitch->getSeventvEmotes().globalEmotes()) + *app->getTwitch()->getSeventvEmotes().globalEmotes()) { addString(emote.first.string, TaggedString::Type::SeventvGlobalEmote); } // Bttv Global - for (const auto &emote : *getApp()->twitch->getBttvEmotes().emotes()) + for (const auto &emote : *app->getTwitch()->getBttvEmotes().emotes()) { addString(emote.first.string, TaggedString::Type::BTTVChannelEmote); } // Ffz Global - for (const auto &emote : *getApp()->twitch->getFfzEmotes().emotes()) + for (const auto &emote : *app->getTwitch()->getFfzEmotes().emotes()) { addString(emote.first.string, TaggedString::Type::FFZChannelEmote); } @@ -172,7 +173,8 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) // Emojis if (prefix.startsWith(":")) { - const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes; + const auto &emojiShortCodes = + app->getEmotes()->getEmojis()->getShortCodes(); for (const auto &m : emojiShortCodes) { addString(QString(":%1:").arg(m), TaggedString::Type::Emoji); @@ -231,20 +233,20 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); } #ifdef CHATTERINO_HAVE_PLUGINS - for (const auto &command : getApp()->commands->pluginCommands()) + for (const auto &command : app->getCommands()->pluginCommands()) { addString(command, TaggedString::PluginCommand); } #endif // Custom Chatterino commands - for (const auto &command : getApp()->commands->items) + for (const auto &command : app->getCommands()->items) { addString(command.name, TaggedString::CustomCommand); } // Default Chatterino commands for (const auto &command : - getApp()->commands->getDefaultChatterinoCommandList()) + app->getCommands()->getDefaultChatterinoCommandList()) { addString(command, TaggedString::ChatterinoCommand); } @@ -256,6 +258,19 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } +std::vector CompletionModel::allItems() const +{ + std::shared_lock lock(this->itemsMutex_); + + std::vector results; + results.reserve(this->items_.size()); + for (const auto &item : this->items_) + { + results.push_back(item.string); + } + return results; +} + bool CompletionModel::compareStrings(const QString &a, const QString &b) { // try comparing insensitively, if they are the same then senstively diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 5b46fb2de..affbd3f10 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -6,6 +6,8 @@ #include #include +class InputCompletionTest; + namespace chatterino { class Channel; @@ -60,10 +62,14 @@ public: static bool compareStrings(const QString &a, const QString &b); private: + std::vector allItems() const; + mutable std::shared_mutex itemsMutex_; std::set items_; Channel &channel_; + + friend class ::InputCompletionTest; }; } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index f214d4177..b1b6fb2e7 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -193,7 +193,7 @@ void BttvEmotes::loadEmotes() { if (!Settings::instance().enableBTTVGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } @@ -203,13 +203,18 @@ void BttvEmotes::loadEmotes() auto emotes = this->global_.get(); auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes); if (pair.first) - this->global_.set( + this->setEmotes( std::make_shared(std::move(pair.second))); return pair.first; }) .execute(); } +void BttvEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void BttvEmotes::loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index bca2d4b65..bbdcacccb 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -29,6 +29,7 @@ public: std::shared_ptr emotes() const; boost::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 72c429837..7872d6e68 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -265,7 +265,7 @@ void Emojis::loadEmojiSet() } std::vector> Emojis::parse( - const QString &text) + const QString &text) const { auto result = std::vector>(); int lastParsedEmojiEndIndex = 0; @@ -359,7 +359,7 @@ std::vector> Emojis::parse( return result; } -QString Emojis::replaceShortCodes(const QString &text) +QString Emojis::replaceShortCodes(const QString &text) const { QString ret(text); auto it = this->findShortCodesRegex_.globalMatch(text); @@ -393,4 +393,14 @@ QString Emojis::replaceShortCodes(const QString &text) return ret; } +const EmojiMap &Emojis::getEmojis() const +{ + return this->emojis; +} + +const std::vector &Emojis::getShortCodes() const +{ + return this->shortCodes; +} + } // namespace chatterino diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index 2f1679b6e..217aa1f4a 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -37,16 +37,32 @@ struct EmojiData { using EmojiMap = ConcurrentMap>; -class Emojis +class IEmojis +{ +public: + virtual ~IEmojis() = default; + + virtual std::vector> parse( + const QString &text) const = 0; + virtual const EmojiMap &getEmojis() const = 0; + virtual const std::vector &getShortCodes() const = 0; + virtual QString replaceShortCodes(const QString &text) const = 0; +}; + +class Emojis : public IEmojis { public: void initialize(); void load(); - std::vector> parse(const QString &text); + std::vector> parse( + const QString &text) const override; EmojiMap emojis; std::vector shortCodes; - QString replaceShortCodes(const QString &text); + QString replaceShortCodes(const QString &text) const override; + + const EmojiMap &getEmojis() const override; + const std::vector &getShortCodes() const override; private: void loadEmojis(); diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 180628545..73be0b4dd 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -188,7 +188,7 @@ void FfzEmotes::loadEmotes() { if (!Settings::instance().enableFFZGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } @@ -199,13 +199,18 @@ void FfzEmotes::loadEmotes() .timeout(30000) .onSuccess([this](auto result) -> Outcome { auto parsedSet = parseGlobalEmotes(result.parseJson()); - this->global_.set(std::make_shared(std::move(parsedSet))); + this->setEmotes(std::make_shared(std::move(parsedSet))); return Success; }) .execute(); } +void FfzEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void FfzEmotes::loadChannel( std::weak_ptr channel, const QString &channelID, std::function emoteCallback, diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index be0726f04..e2865fcb5 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -22,6 +22,7 @@ public: std::shared_ptr emotes() const; boost::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel( std::weak_ptr channel, const QString &channelId, std::function emoteCallback, diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 2f7883abc..321e43a65 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -275,7 +275,7 @@ void SeventvEmotes::loadGlobalEmotes() { if (!Settings::instance().enableSevenTVGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setGlobalEmotes(EMPTY_EMOTE_MAP); return; } @@ -289,7 +289,8 @@ void SeventvEmotes::loadGlobalEmotes() auto emoteMap = parseEmotes(parsedEmotes, true); qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() << "7TV Global Emotes"; - this->global_.set(std::make_shared(std::move(emoteMap))); + this->setGlobalEmotes( + std::make_shared(std::move(emoteMap))); return Success; }) @@ -300,6 +301,11 @@ void SeventvEmotes::loadGlobalEmotes() .execute(); } +void SeventvEmotes::setGlobalEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void SeventvEmotes::loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, std::function callback, bool manualRefresh) diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index f978337be..7fea024bb 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -75,6 +75,7 @@ public: std::shared_ptr globalEmotes() const; boost::optional globalEmote(const EmoteName &name) const; void loadGlobalEmotes(); + void setGlobalEmotes(std::shared_ptr emotes); static void loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, std::function callback, diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 9a9a22800..4593c48ba 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -23,7 +23,21 @@ class TwitchChannel; class BttvLiveUpdates; class SeventvEventAPI; -class TwitchIrcServer final : public AbstractIrcServer, public Singleton +class ITwitchIrcServer +{ +public: + virtual ~ITwitchIrcServer() = default; + + virtual const BttvEmotes &getBttvEmotes() const = 0; + virtual const FfzEmotes &getFfzEmotes() const = 0; + virtual const SeventvEmotes &getSeventvEmotes() const = 0; + + // Update this interface with TwitchIrcServer methods as needed +}; + +class TwitchIrcServer final : public AbstractIrcServer, + public Singleton, + public ITwitchIrcServer { public: TwitchIrcServer(); @@ -70,9 +84,9 @@ public: std::unique_ptr bttvLiveUpdates; std::unique_ptr seventvEventAPI; - const BttvEmotes &getBttvEmotes() const; - const FfzEmotes &getFfzEmotes() const; - const SeventvEmotes &getSeventvEmotes() const; + const BttvEmotes &getBttvEmotes() const override; + const FfzEmotes &getFfzEmotes() const override; + const SeventvEmotes &getSeventvEmotes() const override; protected: virtual void initializeConnection(IrcConnection *connection, diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 83bc45e87..b031adea6 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -12,7 +12,7 @@ If you're adding support for a new endpoint, these are the things you should kno 1. Add a virtual function in the `IHelix` class. Naming should reflect the API name as best as possible. 1. Override the virtual function in the `Helix` class. -1. Mock the function in the `MockHelix` class in the `tests/src/HighlightController.cpp` file. +1. Mock the function in the `mock::Helix` class in the `mocks/include/mocks/Helix.hpp` file. 1. (Optional) Make a new error enum for the failure callback. For a simple example, see the `updateUserChatColor` function and its error enum `HelixUpdateUserChatColorError`. diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index 1a65a17d0..f74dab873 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -16,6 +16,7 @@ public: virtual ~IEmotes() = default; virtual ITwitchEmotes *getTwitchEmotes() = 0; + virtual IEmojis *getEmojis() = 0; }; class Emotes final : public IEmotes, public Singleton @@ -32,6 +33,11 @@ public: return &this->twitch; } + IEmojis *getEmojis() final + { + return &this->emojis; + } + TwitchEmotes twitch; Emojis emojis; diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index eb6a2ace7..28eb829ae 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -17,12 +17,7 @@ namespace { using namespace chatterino; - -struct CompletionEmote { - EmotePtr emote; - QString displayName; - QString providerName; -}; +using namespace chatterino::detail; void addEmotes(std::vector &out, const EmoteMap &map, const QString &text, const QString &providerName) @@ -53,33 +48,18 @@ void addEmojis(std::vector &out, const EmojiMap &map, } // namespace -namespace chatterino { +namespace chatterino::detail { -InputCompletionPopup::InputCompletionPopup(QWidget *parent) - : BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless, - BasePopup::DontFocus, BaseWindow::DisableLayoutSave}, - parent) - , model_(this) -{ - this->initLayout(); - - QObject::connect(&this->redrawTimer_, &QTimer::timeout, this, [this] { - if (this->isVisible()) - { - this->ui_.listView->doItemsLayout(); - } - }); - this->redrawTimer_.setInterval(33); -} - -void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) +std::vector buildCompletionEmoteList(const QString &text, + ChannelPtr channel) { std::vector emotes; + auto *app = getIApp(); auto *tc = dynamic_cast(channel.get()); // returns true also for special Twitch channels (/live, /mentions, /whispers, etc.) if (channel->isTwitchChannel()) { - if (auto user = getApp()->accounts->twitch.getCurrent()) + if (auto user = app->getAccounts()->twitch.getCurrent()) { // Twitch Emotes available globally auto emoteData = user->accessEmotes(); @@ -115,21 +95,21 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) } } - if (auto bttvG = getApp()->twitch->getBttvEmotes().emotes()) + if (auto bttvG = app->getTwitch()->getBttvEmotes().emotes()) { addEmotes(emotes, *bttvG, text, "Global BetterTTV"); } - if (auto ffzG = getApp()->twitch->getFfzEmotes().emotes()) + if (auto ffzG = app->getTwitch()->getFfzEmotes().emotes()) { addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ"); } - if (auto seventvG = getApp()->twitch->getSeventvEmotes().globalEmotes()) + if (auto seventvG = app->getTwitch()->getSeventvEmotes().globalEmotes()) { addEmotes(emotes, *seventvG, text, "Global 7TV"); } } - addEmojis(emotes, getApp()->emotes->emojis.emojis, text); + addEmojis(emotes, app->getEmotes()->getEmojis()->getEmojis(), text); // if there is an exact match, put that emote first for (size_t i = 1; i < emotes.size(); i++) @@ -147,6 +127,34 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) } } + return emotes; +} + +} // namespace chatterino::detail + +namespace chatterino { + +InputCompletionPopup::InputCompletionPopup(QWidget *parent) + : BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless, + BasePopup::DontFocus, BaseWindow::DisableLayoutSave}, + parent) + , model_(this) +{ + this->initLayout(); + + QObject::connect(&this->redrawTimer_, &QTimer::timeout, this, [this] { + if (this->isVisible()) + { + this->ui_.listView->doItemsLayout(); + } + }); + this->redrawTimer_.setInterval(33); +} + +void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) +{ + auto emotes = detail::buildCompletionEmoteList(text, std::move(channel)); + this->model_.clear(); int count = 0; diff --git a/src/widgets/splits/InputCompletionPopup.hpp b/src/widgets/splits/InputCompletionPopup.hpp index a75b64563..9f36bb5ae 100644 --- a/src/widgets/splits/InputCompletionPopup.hpp +++ b/src/widgets/splits/InputCompletionPopup.hpp @@ -5,12 +5,29 @@ #include #include +#include namespace chatterino { class Channel; using ChannelPtr = std::shared_ptr; +struct Emote; +using EmotePtr = std::shared_ptr; + +namespace detail { + + struct CompletionEmote { + EmotePtr emote; + QString displayName; + QString providerName; + }; + + std::vector buildCompletionEmoteList(const QString &text, + ChannelPtr channel); + +} // namespace detail + class GenericListView; class InputCompletionPopup : public BasePopup diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4f4cd2292..ce1677ef3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Filters.cpp ${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/InputCompletion.cpp # Add your new file above this line! ) diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index b2f89e7a5..dc31aad00 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -1,6 +1,5 @@ #include "controllers/highlights/HighlightController.hpp" -#include "Application.hpp" #include "BaseSettings.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightPhrase.hpp" diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp new file mode 100644 index 000000000..e02edb145 --- /dev/null +++ b/tests/src/InputCompletion.cpp @@ -0,0 +1,336 @@ +#include "Application.hpp" +#include "BaseSettings.hpp" +#include "common/Aliases.hpp" +#include "common/CompletionModel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "messages/Emote.hpp" +#include "mocks/EmptyApplication.hpp" +#include "mocks/Helix.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "widgets/splits/InputCompletionPopup.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace chatterino; +using ::testing::Exactly; + +class MockTwitchIrcServer : public ITwitchIrcServer +{ +public: + const BttvEmotes &getBttvEmotes() const override + { + return this->bttv; + } + + const FfzEmotes &getFfzEmotes() const override + { + return this->ffz; + } + + const SeventvEmotes &getSeventvEmotes() const override + { + return this->seventv; + } + + BttvEmotes bttv; + FfzEmotes ffz; + SeventvEmotes seventv; +}; + +class MockApplication : mock::EmptyApplication +{ +public: + AccountController *getAccounts() override + { + return &this->accounts; + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + AccountController accounts; + MockTwitchIrcServer twitch; + Emotes emotes; +}; + +} // namespace + +namespace chatterino { + +class MockChannel : public Channel +{ +public: + MockChannel(const QString &name) + : Channel(name, Channel::Type::Twitch) + { + } +}; + +} // namespace chatterino + +EmotePtr namedEmote(const EmoteName &name) +{ + return std::shared_ptr(new Emote{ + .name{name}, + .images{}, + .tooltip{}, + .zeroWidth{}, + .id{}, + .author{}, + }); +} + +void addEmote(EmoteMap &map, const QString &name) +{ + EmoteName eName{.string{name}}; + map.insert(std::pair(eName, namedEmote(eName))); +} + +static QString DEFAULT_SETTINGS = R"!( +{ + "accounts": { + "uid117166826": { + "username": "testaccount_420", + "userID": "117166826", + "clientID": "abc", + "oauthToken": "def" + }, + "current": "testaccount_420" + } +})!"; + +class InputCompletionTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Write default settings to the mock settings json file + ASSERT_TRUE(QDir().mkpath("/tmp/c2-tests")); + + QFile settingsFile("/tmp/c2-tests/settings.json"); + ASSERT_TRUE(settingsFile.open(QIODevice::WriteOnly | QIODevice::Text)); + ASSERT_GT(settingsFile.write(DEFAULT_SETTINGS.toUtf8()), 0); + ASSERT_TRUE(settingsFile.flush()); + settingsFile.close(); + + // Initialize helix client + this->mockHelix = std::make_unique(); + initializeHelix(this->mockHelix.get()); + EXPECT_CALL(*this->mockHelix, loadBlocks).Times(Exactly(1)); + EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1)); + + this->mockApplication = std::make_unique(); + this->settings = std::make_unique("/tmp/c2-tests"); + this->paths = std::make_unique(); + + this->mockApplication->accounts.initialize(*this->settings, + *this->paths); + this->mockApplication->emotes.initialize(*this->settings, *this->paths); + + this->channelPtr = std::make_shared("icelys"); + this->completionModel = + std::make_unique(*this->channelPtr); + + this->initializeEmotes(); + } + + void TearDown() override + { + ASSERT_TRUE(QDir("/tmp/c2-tests").removeRecursively()); + this->mockApplication.reset(); + this->settings.reset(); + this->paths.reset(); + this->mockHelix.reset(); + this->completionModel.reset(); + this->channelPtr.reset(); + } + + std::unique_ptr mockApplication; + std::unique_ptr settings; + std::unique_ptr paths; + std::unique_ptr mockHelix; + + ChannelPtr channelPtr; + std::unique_ptr completionModel; + +private: + void initializeEmotes() + { + auto bttvEmotes = std::make_shared(); + addEmote(*bttvEmotes, "FeelsGoodMan"); + addEmote(*bttvEmotes, "FeelsBadMan"); + addEmote(*bttvEmotes, "FeelsBirthdayMan"); + addEmote(*bttvEmotes, "Aware"); + addEmote(*bttvEmotes, "Clueless"); + addEmote(*bttvEmotes, "SaltyCorn"); + addEmote(*bttvEmotes, ":)"); + addEmote(*bttvEmotes, ":-)"); + addEmote(*bttvEmotes, "B-)"); + addEmote(*bttvEmotes, "Clap"); + this->mockApplication->twitch.bttv.setEmotes(std::move(bttvEmotes)); + + auto ffzEmotes = std::make_shared(); + addEmote(*ffzEmotes, "LilZ"); + addEmote(*ffzEmotes, "ManChicken"); + addEmote(*ffzEmotes, "CatBag"); + this->mockApplication->twitch.ffz.setEmotes(std::move(ffzEmotes)); + + auto seventvEmotes = std::make_shared(); + addEmote(*seventvEmotes, "Clap"); + addEmote(*seventvEmotes, "Clap2"); + this->mockApplication->twitch.seventv.setGlobalEmotes( + std::move(seventvEmotes)); + } + +protected: + auto queryEmoteCompletion(const QString &fullQuery) + { + // At the moment, buildCompletionEmoteList does not want the ':'. + QString normalizedQuery = fullQuery; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + } + + return chatterino::detail::buildCompletionEmoteList(normalizedQuery, + this->channelPtr); + } + + auto queryTabCompletion(const QString &fullQuery, bool isFirstWord) + { + this->completionModel->refresh(fullQuery, isFirstWord); + return this->completionModel->allItems(); + } +}; + +TEST_F(InputCompletionTest, EmoteNameFiltering) +{ + auto completion = queryEmoteCompletion(":feels"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, "FeelsBirthdayMan"); + ASSERT_EQ(completion[1].displayName, "FeelsBadMan"); + ASSERT_EQ(completion[2].displayName, "FeelsGoodMan"); + + completion = queryEmoteCompletion(":)"); + ASSERT_EQ(completion.size(), 3); + ASSERT_EQ(completion[0].displayName, ":)"); // Exact match with : prefix + ASSERT_EQ(completion[1].displayName, ":-)"); + ASSERT_EQ(completion[2].displayName, "B-)"); + + completion = queryEmoteCompletion(":cat"); + ASSERT_TRUE(completion.size() >= 2); + // emoji exact match comes first + ASSERT_EQ(completion[0].displayName, "cat"); + // FFZ emote is prioritized over any other matching emojis + ASSERT_EQ(completion[1].displayName, "CatBag"); +} + +TEST_F(InputCompletionTest, EmoteExactNameMatching) +{ + auto completion = queryEmoteCompletion(":cat"); + ASSERT_TRUE(completion.size() >= 2); + // emoji exact match comes first + ASSERT_EQ(completion[0].displayName, "cat"); + // FFZ emote is prioritized over any other matching emojis + ASSERT_EQ(completion[1].displayName, "CatBag"); + + // not exactly "salt", SaltyCorn BTTV emote comes first + completion = queryEmoteCompletion(":sal"); + ASSERT_TRUE(completion.size() >= 3); + ASSERT_EQ(completion[0].displayName, "SaltyCorn"); + ASSERT_EQ(completion[1].displayName, "green_salad"); + ASSERT_EQ(completion[2].displayName, "salt"); + + // exactly "salt", emoji comes first + completion = queryEmoteCompletion(":salt"); + ASSERT_TRUE(completion.size() >= 2); + ASSERT_EQ(completion[0].displayName, "salt"); + ASSERT_EQ(completion[1].displayName, "SaltyCorn"); +} + +TEST_F(InputCompletionTest, EmoteProviderOrdering) +{ + auto completion = queryEmoteCompletion(":clap"); + // Current implementation leads to the exact first match being ignored when + // checking for exact matches. This is probably not intended behavior but + // this test is just verifying that the implementation stays the same. + // + // Initial ordering after filtering all available emotes: + // 1. Clap - BTTV + // 2. Clap - 7TV + // 3. Clap2 - 7TV + // 4. clapper - Emoji + // 5. clap - Emoji + // + // The 'exact match' starts looking at the second element and ends up swapping + // #2 with #1 despite #1 already being an exact match. + ASSERT_TRUE(completion.size() >= 5); + ASSERT_EQ(completion[0].displayName, "Clap"); + ASSERT_EQ(completion[0].providerName, "Global 7TV"); + ASSERT_EQ(completion[1].displayName, "Clap"); + ASSERT_EQ(completion[1].providerName, "Global BetterTTV"); + ASSERT_EQ(completion[2].displayName, "Clap2"); + ASSERT_EQ(completion[2].providerName, "Global 7TV"); + ASSERT_EQ(completion[3].displayName, "clapper"); + ASSERT_EQ(completion[3].providerName, "Emoji"); + ASSERT_EQ(completion[4].displayName, "clap"); + ASSERT_EQ(completion[4].providerName, "Emoji"); +} + +TEST_F(InputCompletionTest, TabCompletionEmote) +{ + auto completion = queryTabCompletion(":feels", false); + ASSERT_EQ(completion.size(), 0); // : prefix matters here + + // no : prefix defaults to emote completion + completion = queryTabCompletion("feels", false); + ASSERT_EQ(completion.size(), 3); + // note: different order from : menu + ASSERT_EQ(completion[0], "FeelsBadMan "); + ASSERT_EQ(completion[1], "FeelsBirthdayMan "); + ASSERT_EQ(completion[2], "FeelsGoodMan "); + + // no : prefix, emote completion. Duplicate Clap should be removed + completion = queryTabCompletion("cla", false); + ASSERT_EQ(completion.size(), 2); + ASSERT_EQ(completion[0], "Clap "); + ASSERT_EQ(completion[1], "Clap2 "); + + completion = queryTabCompletion("peepoHappy", false); + ASSERT_EQ(completion.size(), 0); // no peepoHappy emote + + completion = queryTabCompletion("Aware", false); + ASSERT_EQ(completion.size(), 1); + ASSERT_EQ(completion[0], "Aware "); // trailing space added +} + +TEST_F(InputCompletionTest, TabCompletionEmoji) +{ + auto completion = queryTabCompletion(":cla", false); + ASSERT_EQ(completion.size(), 8); + ASSERT_EQ(completion[0], ":clap: "); + ASSERT_EQ(completion[1], ":clap_tone1: "); + ASSERT_EQ(completion[2], ":clap_tone2: "); + ASSERT_EQ(completion[3], ":clap_tone3: "); + ASSERT_EQ(completion[4], ":clap_tone4: "); + ASSERT_EQ(completion[5], ":clap_tone5: "); + ASSERT_EQ(completion[6], ":clapper: "); + ASSERT_EQ(completion[7], ":classical_building: "); +} diff --git a/tests/src/TwitchMessageBuilder.cpp b/tests/src/TwitchMessageBuilder.cpp index e167a7773..db8675ca1 100644 --- a/tests/src/TwitchMessageBuilder.cpp +++ b/tests/src/TwitchMessageBuilder.cpp @@ -1,6 +1,5 @@ #include "providers/twitch/TwitchMessageBuilder.hpp" -#include "Application.hpp" #include "common/Channel.hpp" #include "messages/MessageBuilder.hpp" #include "mocks/EmptyApplication.hpp" @@ -27,6 +26,7 @@ public: { return &this->emotes; } + IUserDataController *getUserData() override { return &this->userData; From bd4f6f3a1f0320eff82f04148b4c1dd1ad58b069 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 21 May 2023 19:42:40 +0200 Subject: [PATCH 13/83] Configure CMake for `clang-tidy` separately (#4648) Co-authored-by: Rasmus Karlsson --- .github/workflows/build.yml | 17 ++++++++++++++++- CHANGELOG.md | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27bb7e51e..f11e9fcbf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -278,10 +278,25 @@ jobs: if: matrix.clang-tidy-review && github.event_name == 'pull_request' uses: ZedThree/clang-tidy-review@v0.13.0 with: - build_dir: build + build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true exclude: "tests/*,lib/*" + cmake_command: >- + cmake -S. -Bbuild-clang-tidy + -DCMAKE_BUILD_TYPE=Release + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On + -DUSE_PRECOMPILED_HEADERS=OFF + -DCMAKE_EXPORT_COMPILE_COMMANDS=On + -DCHATTERINO_LTO=Off + -DCHATTERINO_PLUGINS=On + -DBUILD_WITH_QT6=Off + apt_packages: >- + qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, + libsecret-1-dev, + libboost-dev, libboost-system-dev, libboost-filesystem-dev, + libssl-dev, + rapidjson-dev - name: clang-tidy-review upload if: matrix.clang-tidy-review && github.event_name == 'pull_request' diff --git a/CHANGELOG.md b/CHANGELOG.md index bd82de959..be24b4f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) +- Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) ## 2.4.4 From ba6fae6db9403b4e322e5c207cccadcc0e10bbca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 May 2023 23:47:53 +0200 Subject: [PATCH 14/83] Bump jurplel/install-qt-action from 3.2.0 to 3.2.1 (#4635) Bumps [jurplel/install-qt-action](https://github.com/jurplel/install-qt-action) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/jurplel/install-qt-action/releases) - [Commits](https://github.com/jurplel/install-qt-action/compare/v3.2.0...v3.2.1) --- updated-dependencies: - dependency-name: jurplel/install-qt-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f11e9fcbf..ef0174d66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,7 +107,7 @@ jobs: - name: Install Qt5 if: startsWith(matrix.qt-version, '5.') - uses: jurplel/install-qt-action@v3.2.0 + uses: jurplel/install-qt-action@v3.2.1 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 @@ -115,7 +115,7 @@ jobs: - name: Install Qt6 if: startsWith(matrix.qt-version, '6.') - uses: jurplel/install-qt-action@v3.2.0 + uses: jurplel/install-qt-action@v3.2.1 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 967c34e36..7ef41f8b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: key: ${{ runner.os }}-QtCache-${{ matrix.qt-version }} - name: Install Qt - uses: jurplel/install-qt-action@v3.2.0 + uses: jurplel/install-qt-action@v3.2.1 with: cache: true cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }} From c1dda281df3f6a46005c96c83b1503ffeacfac6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 10:43:31 +0200 Subject: [PATCH 15/83] Bump ZedThree/clang-tidy-review from 0.13.0 to 0.13.1 (#4651) Bumps [ZedThree/clang-tidy-review](https://github.com/ZedThree/clang-tidy-review) from 0.13.0 to 0.13.1. - [Release notes](https://github.com/ZedThree/clang-tidy-review/releases) - [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md) - [Commits](https://github.com/ZedThree/clang-tidy-review/compare/v0.13.0...v0.13.1) --- updated-dependencies: - dependency-name: ZedThree/clang-tidy-review dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/post-clang-tidy-review.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef0174d66..59aa18031 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -276,7 +276,7 @@ jobs: - name: clang-tidy review if: matrix.clang-tidy-review && github.event_name == 'pull_request' - uses: ZedThree/clang-tidy-review@v0.13.0 + uses: ZedThree/clang-tidy-review@v0.13.1 with: build_dir: build-clang-tidy config_file: ".clang-tidy" @@ -300,7 +300,7 @@ jobs: - name: clang-tidy-review upload if: matrix.clang-tidy-review && github.event_name == 'pull_request' - uses: ZedThree/clang-tidy-review/upload@v0.13.0 + uses: ZedThree/clang-tidy-review/upload@v0.13.1 - name: Package - AppImage (Ubuntu) if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index 0254d51d2..9b9d73c86 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -12,6 +12,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: ZedThree/clang-tidy-review/post@v0.13.0 + - uses: ZedThree/clang-tidy-review/post@v0.13.1 with: lgtm_comment_body: "" From 2264c44f10a3dc2cfd6c6e67bea5f0d13604f985 Mon Sep 17 00:00:00 2001 From: nerix Date: Wed, 24 May 2023 23:47:03 +0200 Subject: [PATCH 16/83] Include Tests and Benchmarks in `clang-tidy` CI (#4653) Co-authored-by: Rasmus Karlsson --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59aa18031..7ac21fb45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -281,7 +281,7 @@ jobs: build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true - exclude: "tests/*,lib/*" + exclude: "lib/*" cmake_command: >- cmake -S. -Bbuild-clang-tidy -DCMAKE_BUILD_TYPE=Release @@ -291,12 +291,15 @@ jobs: -DCHATTERINO_LTO=Off -DCHATTERINO_PLUGINS=On -DBUILD_WITH_QT6=Off + -DBUILD_TESTS=On + -DBUILD_BENCHMARKS=On apt_packages: >- qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, libsecret-1-dev, libboost-dev, libboost-system-dev, libboost-filesystem-dev, libssl-dev, - rapidjson-dev + rapidjson-dev, + libbenchmark-dev - name: clang-tidy-review upload if: matrix.clang-tidy-review && github.event_name == 'pull_request' From 1bc423d9c456a275ee08411fa77ec2b468c8b954 Mon Sep 17 00:00:00 2001 From: nerix Date: Fri, 26 May 2023 14:54:23 +0200 Subject: [PATCH 17/83] Make tests more platform agnostic (#4650) Use QTemporaryDir to create the test directory Add option to use httpbin over local docker Increase delay for opening a connection Co-authored-by: Rasmus Karlsson --- benchmarks/src/Highlights.cpp | 4 +++- benchmarks/src/main.cpp | 10 +++++++--- tests/CMakeLists.txt | 6 ++++++ tests/src/HighlightController.cpp | 12 ++++++++---- tests/src/InputCompletion.cpp | 19 +++++++++++++++---- tests/src/NetworkRequest.cpp | 5 ++++- tests/src/TwitchPubSubClient.cpp | 8 ++++---- tests/src/main.cpp | 16 +++++++++------- 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp index 303cb6612..c363e508c 100644 --- a/benchmarks/src/Highlights.cpp +++ b/benchmarks/src/Highlights.cpp @@ -12,6 +12,7 @@ #include #include #include +#include using namespace chatterino; @@ -66,7 +67,8 @@ public: static void BM_HighlightTest(benchmark::State &state) { MockApplication mockApplication; - Settings settings("/tmp/c2-mock"); + QTemporaryDir settingsDir; + Settings settings(settingsDir.path()); std::string message = R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))"; diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index 5f4c967f8..78ecf5b4d 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace chatterino; @@ -12,12 +13,15 @@ int main(int argc, char **argv) ::benchmark::Initialize(&argc, argv); - // Ensure settings are initialized before any tests are run - chatterino::Settings settings("/tmp/c2-empty-mock"); + // Ensure settings are initialized before any benchmarks are run + QTemporaryDir settingsDir; + settingsDir.setAutoRemove(false); // we'll remove it manually + chatterino::Settings settings(settingsDir.path()); - QtConcurrent::run([&app] { + QtConcurrent::run([&app, &settingsDir]() mutable { ::benchmark::RunSpecifiedBenchmarks(); + settingsDir.remove(); app.exit(0); }); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ce1677ef3..e6f7b5bf4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,7 @@ project(chatterino-test) +option(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN "Use public httpbin for testing network requests" OFF) + set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp @@ -54,6 +56,10 @@ if(CHATTERINO_ENABLE_LTO) PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) endif() +if(CHATTERINO_TEST_USE_PUBLIC_HTTPBIN) + target_compile_definitions(${PROJECT_NAME} PRIVATE CHATTERINO_TEST_USE_PUBLIC_HTTPBIN) +endif() + # gtest_add_tests manages to discover the tests because it looks through the source files # HOWEVER, it fails to run, because we have some bug that causes the QApplication exit to stall when no network requests have been made. # ctest runs each test individually, so for now we require that testers just run the ./bin/chatterino-test binary without any filters applied diff --git a/tests/src/HighlightController.cpp b/tests/src/HighlightController.cpp index dc31aad00..c1a7235d5 100644 --- a/tests/src/HighlightController.cpp +++ b/tests/src/HighlightController.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using namespace chatterino; using ::testing::Exactly; @@ -178,9 +179,9 @@ protected: void SetUp() override { // Write default settings to the mock settings json file - ASSERT_TRUE(QDir().mkpath("/tmp/c2-tests")); + this->settingsDir_ = std::make_unique(); - QFile settingsFile("/tmp/c2-tests/settings.json"); + QFile settingsFile(this->settingsDir_->filePath("settings.json")); ASSERT_TRUE(settingsFile.open(QIODevice::WriteOnly | QIODevice::Text)); ASSERT_GT(settingsFile.write(DEFAULT_SETTINGS.toUtf8()), 0); ASSERT_TRUE(settingsFile.flush()); @@ -194,7 +195,7 @@ protected: EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1)); this->mockApplication = std::make_unique(); - this->settings = std::make_unique("/tmp/c2-tests"); + this->settings = std::make_unique(this->settingsDir_->path()); this->paths = std::make_unique(); this->controller = std::make_unique(); @@ -206,16 +207,19 @@ protected: void TearDown() override { - ASSERT_TRUE(QDir("/tmp/c2-tests").removeRecursively()); this->mockApplication.reset(); this->settings.reset(); this->paths.reset(); this->controller.reset(); + this->settingsDir_.reset(); + delete this->mockHelix; } + std::unique_ptr settingsDir_; + std::unique_ptr mockApplication; std::unique_ptr settings; std::unique_ptr paths; diff --git a/tests/src/InputCompletion.cpp b/tests/src/InputCompletion.cpp index e02edb145..fd539cfe1 100644 --- a/tests/src/InputCompletion.cpp +++ b/tests/src/InputCompletion.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace { @@ -122,9 +123,9 @@ protected: void SetUp() override { // Write default settings to the mock settings json file - ASSERT_TRUE(QDir().mkpath("/tmp/c2-tests")); + this->settingsDir_ = std::make_unique(); - QFile settingsFile("/tmp/c2-tests/settings.json"); + QFile settingsFile(this->settingsDir_->filePath("settings.json")); ASSERT_TRUE(settingsFile.open(QIODevice::WriteOnly | QIODevice::Text)); ASSERT_GT(settingsFile.write(DEFAULT_SETTINGS.toUtf8()), 0); ASSERT_TRUE(settingsFile.flush()); @@ -137,7 +138,7 @@ protected: EXPECT_CALL(*this->mockHelix, update).Times(Exactly(1)); this->mockApplication = std::make_unique(); - this->settings = std::make_unique("/tmp/c2-tests"); + this->settings = std::make_unique(this->settingsDir_->path()); this->paths = std::make_unique(); this->mockApplication->accounts.initialize(*this->settings, @@ -153,15 +154,18 @@ protected: void TearDown() override { - ASSERT_TRUE(QDir("/tmp/c2-tests").removeRecursively()); this->mockApplication.reset(); this->settings.reset(); this->paths.reset(); this->mockHelix.reset(); this->completionModel.reset(); this->channelPtr.reset(); + + this->settingsDir_.reset(); } + std::unique_ptr settingsDir_; + std::unique_ptr mockApplication; std::unique_ptr settings; std::unique_ptr paths; @@ -222,8 +226,14 @@ protected: TEST_F(InputCompletionTest, EmoteNameFiltering) { + // The completion doesn't guarantee an ordering for a specific category of emotes. + // This tests a specific implementation of the underlying std::unordered_map, + // so depending on the standard library used when compiling, this might yield + // different results. + auto completion = queryEmoteCompletion(":feels"); ASSERT_EQ(completion.size(), 3); + // all these matches are BTTV global emotes ASSERT_EQ(completion[0].displayName, "FeelsBirthdayMan"); ASSERT_EQ(completion[1].displayName, "FeelsBadMan"); ASSERT_EQ(completion[2].displayName, "FeelsGoodMan"); @@ -231,6 +241,7 @@ TEST_F(InputCompletionTest, EmoteNameFiltering) completion = queryEmoteCompletion(":)"); ASSERT_EQ(completion.size(), 3); ASSERT_EQ(completion[0].displayName, ":)"); // Exact match with : prefix + // all these matches are Twitch global emotes ASSERT_EQ(completion[1].displayName, ":-)"); ASSERT_EQ(completion[2].displayName, "B-)"); diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index 8f10180a0..85ed0246b 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -12,8 +12,11 @@ using namespace chatterino; namespace { -// Change to http://httpbin.org if you don't want to run the docker image yourself to test this +#ifdef CHATTERINO_TEST_USE_PUBLIC_HTTPBIN +const char *const HTTPBIN_BASE_URL = "http://httpbin.org"; +#else const char *const HTTPBIN_BASE_URL = "http://127.0.0.1:9051"; +#endif QString getStatusURL(int code) { diff --git a/tests/src/TwitchPubSubClient.cpp b/tests/src/TwitchPubSubClient.cpp index 86739fc4a..9684fd248 100644 --- a/tests/src/TwitchPubSubClient.cpp +++ b/tests/src/TwitchPubSubClient.cpp @@ -48,7 +48,7 @@ TEST(TwitchPubSubClient, ServerRespondsToPings) pubSub->listenToTopic("test"); - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(150ms); ASSERT_EQ(pubSub->diag.connectionsOpened, 1); ASSERT_EQ(pubSub->diag.connectionsClosed, 0); @@ -211,7 +211,7 @@ TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep) pubSub->listenToTopic("test"); } - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(150ms); ASSERT_EQ(pubSub->diag.connectionsOpened, 2); ASSERT_EQ(pubSub->diag.connectionsClosed, 0); @@ -242,7 +242,7 @@ TEST(TwitchPubSubClient, ReceivedWhisper) pubSub->listenToTopic("whispers.123456"); - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(150ms); ASSERT_EQ(pubSub->diag.connectionsOpened, 1); ASSERT_EQ(pubSub->diag.connectionsClosed, 0); @@ -324,7 +324,7 @@ TEST(TwitchPubSubClient, MissingToken) pubSub->listenToTopic("chat_moderator_actions.123456.123456"); - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(150ms); ASSERT_EQ(pubSub->diag.connectionsOpened, 1); ASSERT_EQ(pubSub->diag.connectionsClosed, 0); diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 098d5b366..421ea6e33 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -1,14 +1,10 @@ #include "common/NetworkManager.hpp" -#include "common/NetworkRequest.hpp" -#include "common/NetworkResult.hpp" -#include "common/Outcome.hpp" -#include "common/QLogging.hpp" -#include "providers/twitch/api/Helix.hpp" #include "singletons/Settings.hpp" #include #include #include +#include #include #include @@ -25,17 +21,23 @@ int main(int argc, char **argv) #ifdef SUPPORT_QT_NETWORK_TESTS QApplication app(argc, argv); + // make sure to always debug-log + QLoggingCategory::setFilterRules("*.debug=true"); chatterino::NetworkManager::init(); // Ensure settings are initialized before any tests are run - chatterino::Settings settings("/tmp/c2-empty-test"); + QTemporaryDir settingsDir; + settingsDir.setAutoRemove(false); // we'll remove it manually + qDebug() << "Settings directory:" << settingsDir.path(); + chatterino::Settings settings(settingsDir.path()); - QtConcurrent::run([&app] { + QtConcurrent::run([&app, &settingsDir]() mutable { auto res = RUN_ALL_TESTS(); chatterino::NetworkManager::deinit(); + settingsDir.remove(); app.exit(res); }); From c6c884df70adce4dc527d27143463522f049efe2 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 27 May 2023 10:38:25 +0000 Subject: [PATCH 18/83] Add an indicator in the title bar if Streamer Mode is active (#4410) --- CHANGELOG.md | 1 + resources/buttons/streamerModeEnabledDark.png | Bin 0 -> 3574 bytes resources/buttons/streamerModeEnabledDark.svg | 13 +++++ .../buttons/streamerModeEnabledLight.png | Bin 0 -> 3528 bytes .../buttons/streamerModeEnabledLight.svg | 13 +++++ src/Application.hpp | 4 ++ src/singletons/Settings.cpp | 6 +++ src/util/StreamerMode.cpp | 4 ++ src/widgets/Notebook.cpp | 41 ++++++++++++++++ src/widgets/Notebook.hpp | 6 +++ src/widgets/Window.cpp | 46 ++++++++++++++++++ src/widgets/Window.hpp | 5 ++ src/widgets/dialogs/SettingsDialog.cpp | 17 ++++++- src/widgets/dialogs/SettingsDialog.hpp | 2 + src/widgets/helper/SettingsDialogTab.hpp | 1 + src/widgets/helper/TitlebarButton.hpp | 3 +- src/widgets/settingspages/AboutPage.cpp | 3 ++ 17 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 resources/buttons/streamerModeEnabledDark.png create mode 100644 resources/buttons/streamerModeEnabledDark.svg create mode 100644 resources/buttons/streamerModeEnabledLight.png create mode 100644 resources/buttons/streamerModeEnabledLight.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index be24b4f96..7085d62d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) diff --git a/resources/buttons/streamerModeEnabledDark.png b/resources/buttons/streamerModeEnabledDark.png new file mode 100644 index 0000000000000000000000000000000000000000..7ab2b1d8b3fcd6f5235a479b6c40e0a54fd7d7aa GIT binary patch literal 3574 zcma)Cq)Mx+NqWJ!Le4ARr+qA&d@bX(cy8V&o(qQcAi+ zoHYCU-289f_u_ZX=Ukkt-{+k3J&7hpI#d)a6aWB#N>5kQ>~D7dSLCFB<3>)fJODu8 z>!qP#;^pcJ0Pv?jPggV`f-uCPc=R>7=~_%0=ed%!h)rn?QnYfx54FX;-_?eYhX{Ax zyALB_eG?t$xmoj*(^F_(kUWMfVSeG-o2NGT{zI-2;pX$rG34k<^BT|{hn$rqY7fUI z6B8|(!EAz18*Tua?%xB?M{}rNYqZ!qoy(hdvqHOVSD$rs%w=Rzr&6yAtcbtqlcXw6 zDJe$id@-IJ_~%vHtJ?}7e|X(*&3q?@j|Vr!^U2!V=&DffJyqmIf9T-Yy)=sJz=e!E z#k8yY#*(Nn;O8cPXhC!_#8bK^F8RV*gJs*i&D%YvxqbyP2zUtVdx@{<|KWB-Rr2oT zzR$g{EMT0~gt?6>u=EZr`MyFQ{l{Y|l9TjY)Lf|THUhRZOYg7T>)x|2rfW1^SYpMJ zdqUe~=N~ycbwA9%k?J;0dpY?Zo@|(hAc>#H6npGUqZa@G6xGv&m?IpI>?7W0Sv)}PENo4K ziVf9l%(%UaOkB$}0gzah(!>5IOxOKpti{pJ3)$(Fq}tQM4;x!@ zy)&dU$^juv2Kq~WAS!aI1hT%(j~{Ph;>v}o z{gruBqo_?T(FsqpC}FC*?Tgb->mG)3F|+e(2rdx3INN>#_zeYr;3;}!CW*O&{`4!} z(}OiTpdvl1ru1J#d--%vT3`1Ns%2$UG_CH_uW+}G%MP7Jl)DIx5adv*-UColMqdcZ zNdGi|se!6LttJ=TwGtYA8JCrdDq+}6N{taG>mi}SGf(P@0ZLjc>W?kCn#uu(Cmo_d zA74izwiLoF{KlEV6!s4&wzZOOO^x|XIX&F9EhpmImv#4tRPjW77Kmeu&Fz9B7(IKB z??Z&i3^}?XFEIqqr~XWg0A~(dE&YH+4nA-WqJrJggNYRzk^EV7ogwN@U+D&Vv*+N^2wqvIvmvvxFdk3dOdJ2PTSW zQ3n=+L&muHV6OdiM@)#t)Y-@0gy$R%%}22`J$hd?>D1?i*@#(>hifQ=UmPut&Ilgt zU$|d)fl&hi<-*<&s9ji`_#xsL#k$neSn;@Pcp{cfY$6`s&??yhkpjkW0N`Y#pb+;(ccLM!);AEMh{o%@$+)kow8fV8-nblzusjeh4ODB|gU3$Id;6RW6>BTQ4e8OR zl!p~bV&AK)w~Nm24AA0KO#9M5cxBIAW~_W0ge)(#5e2mvf_ZocwTuqJcEjs6jkDRC zm4WX#E~p6%4>6MX7YCA@9!fr7x+kW3j6uLIC#Q@6=DSoUQ5xaPk!nO zE!iz)afMSSY>tv_$a=9f)>x**EN7Fj8|PeI;aHU2GplMwPkWQ1_a=ygLf-`l2NZ?U zeN;f_-_UkL)qWuq)OCRzV|HyGVQz}23mZw3*9GwodF;9P7TqGZr^H)^1gngY#AWx`K!{ld&fF#e>J~glF*;P-!xVk#4 z4Q?m%#wBTKC0Ca6AZk^~ovBhCMsUdnZ`AqB1!2WVN=3y}nH9ul4Mm$;2$r)dryyx}6u{iZ5G6{YJ+UXQcRLn{-~4`Jo*0QwQ@hxn%z%dxHs6{5ZGaDd#tvKG z%!idKh0hnVx*K{wO&BDI zPBE-gmp1JK{oV1Jj9UK?PA3ZHcIqfO?lW)eK9@SqFh97dIlL@eg~UdPJ;T2CkbQ}` zea~k4(*ABMfiC;WC5G$($?~m0NpbnFUC&aV#=N9KhHL5TC|S%c8g#X~9~&h-`xD~` zdXKW(il*Z(=mQb#P~g7PbdU#+AU>NYl;UWR_1y)2d}$QwkcWe6Ghv(wHEct4`_O9A z5>LT74i!_?U}c_ZooI<~FB&lfBXG1;_Ag58jyb8NneJf2<9uiLBlF%K#_6JuibZut z%>1YA!L_m}M7Z*?=DcE!#bcv>$6x2iX2lY6l-%~2BH8|HqrV#D*NoX$s;Gj$nMi?#LJO2(XII)-7L$1Zwo*KNh^xE+a@vIoEP z;8oQA8Kwiwy_a7X^Q7J6`B=^#LxyDL(+X>_P|FYY^+wBrpZ^s6JKCl^0Y49ZV=!G{ z{;Tq$Ly8|3`+eBnSSz(52xL-!_jIz9MMx6!rLFe1-sA}I&64SvcW>eX)9z)1m3V4Y zdbJu;qkBO&y)l z8_ds#b^3uG!L(D~_nkoBR#z19ups82abpReOM2_*NWo!@6(vFGHeHYrS5p^i4+iCA zMWM7f5&G#Y9iZbNL3?>-hZ zDaTyyoH5t6ko=->4f?yYZxC($s5r=P?i7kFtYT65me=t0rfo@QODR}ya}CM5dZNQN z1VK~?sn}}dv!%mBe#nenH@A4nipDhk5iLGqOt1bFO5hO~ZJ^DYA^IO@4{f)?*51gy?v(sH6QRX#cTlKGNTOI^`Qm}dS_vjuc^`u4J zrYE*RQxl^S%Pr4oH^nS_^qZw{L^7%X@Z_P&ObI}f7&2&0Z zp0zGhVFO~{?2)&3Ytkyuc=v!>h)z#DUcf@lq zRy)@7#I1P4^xcb4go5(0{>Xcr^H#og?Sx+_s|MoIdUILRB7j2DjQ + + + + + + diff --git a/resources/buttons/streamerModeEnabledLight.png b/resources/buttons/streamerModeEnabledLight.png new file mode 100644 index 0000000000000000000000000000000000000000..8da207f7959de50f077e683f11cf63f524028431 GIT binary patch literal 3528 zcmai1S5VW7)BS}2hF(NENC!cYqVxb#f}r%?Nr(gx>6p-qq4$n-gbOMl(tD9!q)Q7% zK$IpWBB5N`=Y9QN{tst&&g{(Y!_MqE8*8AaMMKF>2><|%_G1mBzuDnGK*;~b?>T5W z006nWsH+>eI64A=NUC3|;uG*ArpRsq9Swel27}r~zBo-%L;5F)nz=l@FbUW6Du0Oo z-F6B_H!_a+ut?|iO2Tbtu|-ix1YgYJ(z&ZZ6`GNkZ{%+M$@=H~F?{_h^)q7roD8@% zXgYxuj5Bhx^y&VN1n4@C`u)aoXrk2{Z0t|uOgcG?JFQlpx3$fuWzi?k zlq9|>!DzMUPxdchld+fO)FMIEM;Zn8OclQ_N){7f8$Is=T(?#7Px}G}KT{-Ao_jB) zT`4C2%cw8a-NNH%a7@q45J5Wi*Z@&*S97pzL$H3M>vyh4VFbq4pCdzZAXSLpuKTUv zc5X&4BlM1bR^8}jXrY;__nU9{n_3<4JAtEftuoa_04+A<{gR{II#`Zkp0Pm@3{gc z3CZd;*Hvt6xw)2t2rj3*1P+?Bkw8e_1|1==|1s5~wCp zEn&baLd`@@362c!RfQM-5BgDBTB6)#vk5D)kwh=#PP=BW6-j6=Cnh&2pZTC5#st(t zg_=dA{`CvSzD1!-Jag|854ttkt&ZCRbaYq(Q4sK$8GO9+uO(zs@U?sNX&|KEJE+ZE zSthom7Eryo5^0a*oolAyxmG4x^5(Aytfqb;A|crh2L$H4bb=S#Bg`XXId+OLDKb@+ zAC92BC8kBgDW3+oacon=VXI6T4g?JEBS%jjNe!s=iD1fPMyHJlp8ZEmXP+N0R*N_M zSKkD@oLP9Nb|N?ju2|5u!6H9KPOPfUfSi++C}KSVGJK`a$Qj%}i@Y?X$M|(n^-s?;l+GmmP%k z3MFvkPlF1AnvuN3J#MZvFS25sufnvEj*QLrc!=5N?35&w#2va)RJD5;abUQQKRSP2 zcHmq7c8R<5knQkD0ypiss&OD6BL=W4GK)=0y1j6k=$Z#(?if?`h$jpyMlwRg{vuMDZe!de5I@=*_xE%yD>RjtIzSGUeQwRQ4p)LMWa0Sc`QdXIauU8a zW4sabpgj2}tAa4Xjij{4bh2K=^$V>1ZFtcH40c~H4-!rmE zd^#r7qC9*%M_U0`7$iTo&cS)X!-q~0ju`<67VkA@1MDocOn;}`5Rj^M92wX;?KIX} zM?B^2R(Ek@YELo}qn&449sH7K}N#KIq2Ez^?D(kuFq)T0l&svs`NTA$Kx z)#@J&qGid!dk54zCUUr>LSi&0T)&BxMUXS9}i{t#tHKa>o@fhWzu>IK4`zqyX;}zFz-iYl!#38+|KJ_Js z(rSvNbXryG&{C*D!tVT9w?FY8Q@VijMaRPQ(!9G5nwfZBzA;W7Aq&vm_XZbt22}ck z2}+gHOR4RffAoaea?QM(v5pqUzZd%pUs@=#gA%-&ZRe%QUK^H`$^skvPN9(NBe?}W zN4;kZT=I3{>{3w!jnZRZoXG*VL{gsg3ckX=Fcj&9qEuuH_uTf_ybj9o03RT=9|c8? z52wz3YiXPoxhr)1G9?37+ip0L$6E;WXJEkemGpJ)tkOjxeR-Dhbu+O+RaUWh8ZMH^ zD<{B$2h0vAhIO1!FqZQSR5q zoudSxUak1sehV7|sFFoSfqlZSeQKFEnczrGTt`$Gmk^gK`g!w;K^Q{KE#BV)gkkTk`9AWvXcXEGZ-+cCfAF}NV0ecbZjaIfjTG0<8 zLG%iqJI)vPUUGTG-1WYpr7BF9^R_r`k1mxu}IfemG>!o6^gwWLr>HL6T?HsudHafCrSHAKu%@uW{stJy~vJ}WgT3@+_1_#bFj`?eAt#Mjhu_5jzS#& zr$G*bBI?(d2IHBrATm+=*;*npf2*o%X%&%KT2N57Q@Ew8{gwfC9m>ubGm`*j4`u% zj32sC3lgXr8DuKA=Dy70=~vTj^9L7$seCBm*0_$3bMCn&ff;VES;8AQ;x8GEg#5!& zU%qQ6{h^$$LUFlbycBvSY=jAoWLQnJR=%sx=s`9W3r z{@Yf9?Zb}Ol%sye9bE8Zh_hkwn0{^Logpb*TS@jjH{EDe4nFGMuOKiYH7T=Aw5sTM7U`{71WcH`{~w zQJ&*IZvJsp7BpzK9f>c`iWETXnhIxE7i#s$?0rsDC`z@Wq)i$ag+ecc_<}4_<*SSA0GPu8E(Q6 cYBe + + + + + + diff --git a/src/Application.hpp b/src/Application.hpp index 27f1c2f60..6917190bd 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -3,6 +3,8 @@ #include "common/Singleton.hpp" #include "singletons/NativeMessaging.hpp" +#include +#include #include #include @@ -153,6 +155,8 @@ public: } IUserDataController *getUserData() override; + pajlada::Signals::NoArgSignal streamerModeChanged; + private: void addSingleton(Singleton *singleton); void initPubSub(); diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index b32623c42..07e10142c 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -1,5 +1,6 @@ #include "singletons/Settings.hpp" +#include "Application.hpp" #include "controllers/filters/FilterRecord.hpp" #include "controllers/highlights/HighlightBadge.hpp" #include "controllers/highlights/HighlightBlacklistUser.hpp" @@ -136,6 +137,11 @@ Settings::Settings(const QString &settingsDirectory) }, false); #endif + this->enableStreamerMode.connect( + []() { + getApp()->streamerModeChanged.invoke(); + }, + false); } Settings &Settings::instance() diff --git a/src/util/StreamerMode.cpp b/src/util/StreamerMode.cpp index 03f3f59aa..c905b88a0 100644 --- a/src/util/StreamerMode.cpp +++ b/src/util/StreamerMode.cpp @@ -71,6 +71,7 @@ bool isInStreamerMode() p.exitStatus() == QProcess::NormalExit) { cache = (p.exitCode() == 0); + getApp()->streamerModeChanged.invoke(); return (p.exitCode() == 0); } @@ -89,6 +90,7 @@ bool isInStreamerMode() qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; cache = false; + getApp()->streamerModeChanged.invoke(); return false; #endif @@ -122,6 +124,7 @@ bool isInStreamerMode() if (broadcastingBinaries().contains(processName)) { cache = true; + getApp()->streamerModeChanged.invoke(); return true; } } @@ -133,6 +136,7 @@ bool isInStreamerMode() } cache = false; + getApp()->streamerModeChanged.invoke(); #endif return false; } diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 3f9a9c67b..40a9237a2 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -4,10 +4,12 @@ #include "common/QLogging.hpp" #include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyController.hpp" +#include "singletons/Resources.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/InitUpdateButton.hpp" +#include "util/StreamerMode.hpp" #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/NotebookButton.hpp" @@ -1155,6 +1157,45 @@ void SplitNotebook::addCustomButtons() auto updateBtn = this->addCustomButton(); initUpdateButton(*updateBtn, this->signalHolder_); + + // streamer mode + this->streamerModeIcon_ = this->addCustomButton(); + QObject::connect(this->streamerModeIcon_, &NotebookButton::leftClicked, + [this] { + getApp()->windows->showSettingsDialog( + this, SettingsDialogPreference::StreamerMode); + }); + this->signalHolder_.managedConnect(getApp()->streamerModeChanged, [this]() { + this->updateStreamerModeIcon(); + }); + this->updateStreamerModeIcon(); +} + +void SplitNotebook::updateStreamerModeIcon() +{ + if (this->streamerModeIcon_ == nullptr) + { + return; + } + // A duplicate of this code is in Window class + // That copy handles the TitleBar icon in Window (main window on Windows) + // This one is the one near splits (on linux and mac or non-main windows on Windows) + if (getTheme()->isLightTheme()) + { + this->streamerModeIcon_->setPixmap( + getResources().buttons.streamerModeEnabledLight); + } + else + { + this->streamerModeIcon_->setPixmap( + getResources().buttons.streamerModeEnabledDark); + } + this->streamerModeIcon_->setVisible(isInStreamerMode()); +} + +void SplitNotebook::themeChangedEvent() +{ + this->updateStreamerModeIcon(); } SplitContainer *SplitNotebook::addPage(bool select) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 7cc49cfa6..382b260ec 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -123,6 +123,7 @@ public: SplitContainer *addPage(bool select = false); SplitContainer *getOrAddSelectedPage(); void select(QWidget *page, bool focusPage = true) override; + void themeChangedEvent() override; protected: void showEvent(QShowEvent *event) override; @@ -131,6 +132,11 @@ private: void addCustomButtons(); pajlada::Signals::SignalHolder signalHolder_; + + // Main window on Windows has basically a duplicate of this in Window + NotebookButton *streamerModeIcon_{}; + + void updateStreamerModeIcon(); }; } // namespace chatterino diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index dfac8ba6e..12f605838 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -9,6 +9,7 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Resources.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/Updates.hpp" @@ -186,6 +187,51 @@ void Window::addCustomTitlebarButtons() this->userLabel_->rect().bottomLeft())); }); this->userLabel_->setMinimumWidth(20 * scale()); + + // streamer mode + this->streamerModeTitlebarIcon_ = + this->addTitleBarButton(TitleBarButtonStyle::StreamerMode, [this] { + getApp()->windows->showSettingsDialog( + this, SettingsDialogPreference::StreamerMode); + }); + this->signalHolder_.managedConnect(getApp()->streamerModeChanged, [this]() { + this->updateStreamerModeIcon(); + }); +} + +void Window::updateStreamerModeIcon() +{ + // A duplicate of this code is in SplitNotebook class (in Notebook.{c,h}pp) + // That one is the one near splits (on linux and mac or non-main windows on Windows) + // This copy handles the TitleBar icon in Window (main window on Windows) + if (this->streamerModeTitlebarIcon_ == nullptr) + { + return; + } +#ifdef Q_OS_WIN + assert(this->getType() == WindowType::Main); + if (getTheme()->isLightTheme()) + { + this->streamerModeTitlebarIcon_->setPixmap( + getResources().buttons.streamerModeEnabledLight); + } + else + { + this->streamerModeTitlebarIcon_->setPixmap( + getResources().buttons.streamerModeEnabledDark); + } + this->streamerModeTitlebarIcon_->setVisible(isInStreamerMode()); +#else + // clang-format off + assert(false && "Streamer mode TitleBar icon should not exist on non-Windows OSes"); + // clang-format on +#endif +} + +void Window::themeChangedEvent() +{ + this->updateStreamerModeIcon(); + BaseWindow::themeChangedEvent(); } void Window::addDebugStuff(HotkeyController::HotkeyMap &actions) diff --git a/src/widgets/Window.hpp b/src/widgets/Window.hpp index d99747591..6d25d875c 100644 --- a/src/widgets/Window.hpp +++ b/src/widgets/Window.hpp @@ -31,6 +31,7 @@ public: protected: void closeEvent(QCloseEvent *event) override; bool event(QEvent *event) override; + void themeChangedEvent() override; private: void addCustomTitlebarButtons(); @@ -51,6 +52,10 @@ private: pajlada::Signals::SignalHolder signalHolder_; std::vector bSignals_; + // this is only used on Windows and only on the main window, for the one used otherwise, see SplitNotebook in Notebook.hpp + TitleBarButton *streamerModeTitlebarIcon_ = nullptr; + void updateStreamerModeIcon(); + friend class Notebook; }; diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 5bd218860..1ee94b4cf 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -195,6 +195,11 @@ void SettingsDialog::filterElements(const QString &text) } } +void SettingsDialog::setElementFilter(const QString &query) +{ + this->ui_.search->setText(query); +} + void SettingsDialog::addTabs() { this->ui_.tabContainer->setSpacing(0); @@ -203,7 +208,7 @@ void SettingsDialog::addTabs() // Constructors are wrapped in std::function to remove some strain from first time loading. // clang-format off - this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg"); + this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg", SettingsTabId::General); this->ui_.tabContainer->addSpacing(16); this->addTab([]{return new AccountsPage;}, "Accounts", ":/settings/accounts.svg", SettingsTabId::Accounts); this->addTab([]{return new NicknamesPage;}, "Nicknames", ":/settings/accounts.svg"); @@ -316,10 +321,20 @@ void SettingsDialog::showDialog(QWidget *parent, } break; + case SettingsDialogPreference::StreamerMode: { + instance->selectTab(SettingsTabId::General); + } + break; + default:; } instance->show(); + if (preferredTab == SettingsDialogPreference::StreamerMode) + { + // this is needed because each time the settings are opened, the query is reset + instance->setElementFilter("Streamer Mode"); + } instance->activateWindow(); instance->raise(); instance->setFocus(); diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index ad8f873d3..303842af2 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -27,6 +27,7 @@ class PageHeader : public QFrame enum class SettingsDialogPreference { NoPreference, + StreamerMode, Accounts, ModerationActions, }; @@ -57,6 +58,7 @@ private: void selectTab(SettingsDialogTab *tab, const bool byUser = true); void selectTab(SettingsTabId id); void filterElements(const QString &query); + void setElementFilter(const QString &query); void onOkClicked(); void onCancelClicked(); diff --git a/src/widgets/helper/SettingsDialogTab.hpp b/src/widgets/helper/SettingsDialogTab.hpp index 1c907ad45..921d157f7 100644 --- a/src/widgets/helper/SettingsDialogTab.hpp +++ b/src/widgets/helper/SettingsDialogTab.hpp @@ -15,6 +15,7 @@ class SettingsDialog; enum class SettingsTabId { None, + General, Accounts, Moderation, }; diff --git a/src/widgets/helper/TitlebarButton.hpp b/src/widgets/helper/TitlebarButton.hpp index 85e435005..463582370 100644 --- a/src/widgets/helper/TitlebarButton.hpp +++ b/src/widgets/helper/TitlebarButton.hpp @@ -11,7 +11,8 @@ enum class TitleBarButtonStyle { Unmaximize = 4, Close = 8, User = 16, - Settings = 32 + Settings = 32, + StreamerMode = 64, }; class TitleBarButton : public Button diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 05037621e..9ea3153b9 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -126,6 +126,9 @@ AboutPage::AboutPage() "https://github.com/getsentry/crashpad", ":/licenses/crashpad.txt"); #endif + addLicense(form.getElement(), "Fluent icons", + "https://github.com/microsoft/fluentui-system-icons", + ":/licenses/fluenticons.txt"); } // Attributions From 5ca7d387e4c53bb18ae06416ba4cc1f54605f254 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 27 May 2023 13:33:01 +0200 Subject: [PATCH 19/83] Expand upon test channels (#4655) Available test channels: - `$$$` - Fill up scrollback (1000 messages), then add a new message every 500ms - `$$$:e` - Add a new message every 500ms - `$$$$` - Fill up scrollback (1000 messages), then add a new message every 250ms - `$$$$:e` - Add a new message every 250ms - `$$$$$` - Fill up scrollback (1000 messages), then add a new message every 100ms - `$$$$$:e` - Add a new message every 100ms - `$$$$$$` - Fill up scrollback (1000 messages), then add a new message every 50ms - `$$$$$$:e` - Add a new message every 50ms - `$$$$$$$` - Fill up scrollback (1000 messages), then add a new message every 25ms - `$$$$$$$:e` - Add a new message every 25ms --- CHANGELOG.md | 1 + src/providers/twitch/TwitchIrcServer.cpp | 104 ++++++++++++++++++++--- src/singletons/WindowManager.cpp | 8 ++ 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7085d62d2..568ae0ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) - Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) +- Dev: Expanded upon `$$$` test channels. (#4655) ## 2.4.4 diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index f2b30aef3..b7cc4797b 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -274,24 +274,102 @@ std::shared_ptr TwitchIrcServer::getCustomChannel( return this->liveChannel; } - if (channelName == "$$$") - { - static auto channel = - std::make_shared("$$$", chatterino::Channel::Type::Misc); - static auto getTimer = [&] { + static auto getTimer = [](ChannelPtr channel, int msBetweenMessages, + bool addInitialMessages) { + if (addInitialMessages) + { for (auto i = 0; i < 1000; i++) { channel->addMessage(makeSystemMessage(QString::number(i + 1))); } + } - auto timer = new QTimer; - QObject::connect(timer, &QTimer::timeout, [] { - channel->addMessage( - makeSystemMessage(QTime::currentTime().toString())); - }); - timer->start(500); - return timer; - }(); + auto *timer = new QTimer; + QObject::connect(timer, &QTimer::timeout, [channel] { + channel->addMessage( + makeSystemMessage(QTime::currentTime().toString())); + }); + timer->start(msBetweenMessages); + return timer; + }; + + if (channelName == "$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 500, true); + + return channel; + } + if (channelName == "$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 500, false); + + return channel; + } + if (channelName == "$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 250, true); + + return channel; + } + if (channelName == "$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 250, false); + + return channel; + } + if (channelName == "$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 100, true); + + return channel; + } + if (channelName == "$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 100, false); + + return channel; + } + if (channelName == "$$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 50, true); + + return channel; + } + if (channelName == "$$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 50, false); + + return channel; + } + if (channelName == "$$$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 25, true); + + return channel; + } + if (channelName == "$$$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 25, false); return channel; } diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 15053ccd1..cae51caee 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -631,6 +631,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) } } break; + case Channel::Type::Misc: { + obj.insert("type", "misc"); + obj.insert("name", channel.get()->getName()); + } } } @@ -676,6 +680,10 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) return Irc::instance().getOrAddChannel(descriptor.server_, descriptor.channelName_); } + else if (descriptor.type_ == "misc") + { + return app->twitch->getChannelOrEmpty(descriptor.channelName_); + } return Channel::getEmpty(); } From fb02d59b483f9c5c7a9aa8bf55d4cba87b55141d Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 27 May 2023 12:18:08 +0000 Subject: [PATCH 20/83] Add tools to help debug image GC (#4578) `/debug-force-image-gc` will force garbage collection on all unused images `/debug-force-image-unload` will force unload all images Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 24 ++++++++++ src/messages/Image.cpp | 45 ++++++++++++++++--- src/messages/Image.hpp | 8 ++++ src/util/DebugCount.hpp | 16 +++++++ 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568ae0ee4..45e57cddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Dev: Added test cases for emote and tab completion. (#4644) - Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) - Dev: Expanded upon `$$$` test channels. (#4655) +- Dev: Added tools to help debug image GC. (#4578) ## 2.4.4 diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 7be61286c..3956ec9c8 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -16,6 +16,7 @@ #include "controllers/commands/CommandModel.hpp" #include "controllers/plugins/PluginController.hpp" #include "controllers/userdata/UserDataController.hpp" +#include "messages/Image.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" @@ -38,6 +39,7 @@ #include "util/FormatTime.hpp" #include "util/Helpers.hpp" #include "util/IncognitoBrowser.hpp" +#include "util/PostToThread.hpp" #include "util/Qt.hpp" #include "util/StreamerMode.hpp" #include "util/StreamLink.hpp" @@ -3211,6 +3213,28 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); + this->registerCommand( + "/debug-force-image-gc", + [](const QStringList & /*words*/, auto /*channel*/) -> QString { + runInGuiThread([] { + using namespace chatterino::detail; + auto &iep = ImageExpirationPool::instance(); + iep.freeOld(); + }); + return ""; + }); + + this->registerCommand( + "/debug-force-image-unload", + [](const QStringList & /*words*/, auto /*channel*/) -> QString { + runInGuiThread([] { + using namespace chatterino::detail; + auto &iep = ImageExpirationPool::instance(); + iep.freeAll(); + }); + return ""; + }); + this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shieldoff", &commands::shieldModeOff); diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 2e8616b19..71daf3425 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -76,6 +76,8 @@ namespace detail { 60000); } this->processOffset(); + DebugCount::increase("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); } Frames::~Frames() @@ -91,10 +93,27 @@ namespace detail { { DebugCount::decrease("animated images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", + this->memoryUsage()); this->gifTimerConnection_.disconnect(); } + int64_t Frames::memoryUsage() const + { + int64_t usage = 0; + for (const auto &frame : this->items_) + { + auto sz = frame.image.size(); + auto area = sz.width() * sz.height(); + auto memory = area * frame.image.depth(); + + usage += memory; + } + return usage; + } + void Frames::advance() { this->durationOffset_ += GIF_FRAME_LENGTH; @@ -131,6 +150,9 @@ namespace detail { { DebugCount::decrease("loaded images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", + this->memoryUsage()); this->items_.clear(); this->index_ = 0; @@ -589,14 +611,26 @@ void ImageExpirationPool::removeImagePtr(Image *rawPtr) this->allImages_.erase(rawPtr); } +void ImageExpirationPool::freeAll() +{ + { + std::lock_guard lock(this->mutex_); + for (auto it = this->allImages_.begin(); it != this->allImages_.end();) + { + auto img = it->second.lock(); + img->expireFrames(); + it = this->allImages_.erase(it); + } + } + this->freeOld(); +} + void ImageExpirationPool::freeOld() { std::lock_guard lock(this->mutex_); -# ifndef NDEBUG size_t numExpired = 0; size_t eligible = 0; -# endif auto now = std::chrono::steady_clock::now(); for (auto it = this->allImages_.begin(); it != this->allImages_.end();) @@ -617,17 +651,13 @@ void ImageExpirationPool::freeOld() continue; } -# ifndef NDEBUG ++eligible; -# endif // Check if image has expired and, if so, expire its frame data auto diff = now - img->lastUsed_; if (diff > IMAGE_POOL_IMAGE_LIFETIME) { -# ifndef NDEBUG ++numExpired; -# endif img->expireFrames(); // erase without mutex locking issue it = this->allImages_.erase(it); @@ -641,6 +671,9 @@ void ImageExpirationPool::freeOld() qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/" << eligible << "eligible images"; # endif + DebugCount::set("last image gc: expired", numExpired); + DebugCount::set("last image gc: eligible", eligible); + DebugCount::set("last image gc: left after gc", this->allImages_.size()); } #endif diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 90159a442..98c964eac 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -41,6 +41,7 @@ namespace detail { boost::optional first() const; private: + int64_t memoryUsage() const; void processOffset(); QVector> items_; int index_{0}; @@ -111,6 +112,7 @@ class ImageExpirationPool { private: friend class Image; + friend class CommandController; ImageExpirationPool(); static ImageExpirationPool &instance(); @@ -126,6 +128,12 @@ private: */ void freeOld(); + /* + * Debug function that unloads all images in the pool. This is intended to + * test for possible memory leaks from tracked images. + */ + void freeAll(); + private: // Timer to periodically run freeOld() QTimer *freeTimer_; diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 629cb5fbe..35f9fc621 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -27,6 +27,22 @@ public: reinterpret_cast(it.value())++; } } + + static void set(const QString &name, const int64_t &amount) + { + auto counts = counts_.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->insert(name, amount); + } + else + { + reinterpret_cast(it.value()) = amount; + } + } + static void increase(const QString &name, const int64_t &amount) { auto counts = counts_.access(); From c7b22939d56152ab35067780b2fdf944cc9fc2c4 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 27 May 2023 14:04:30 +0000 Subject: [PATCH 21/83] Improve editing of hotkeys (#4628) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/controllers/hotkeys/ActionNames.hpp | 229 +++++++++++++++++----- src/controllers/hotkeys/HotkeyHelpers.cpp | 20 ++ src/controllers/hotkeys/HotkeyHelpers.hpp | 5 + src/widgets/dialogs/EditHotkeyDialog.cpp | 226 +++++++++++++++------ src/widgets/dialogs/EditHotkeyDialog.hpp | 1 + src/widgets/dialogs/EditHotkeyDialog.ui | 55 ++++-- 7 files changed, 412 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e57cddd..68eadc707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) +- Minor: Improved editing hotkeys. (#4628) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index 8d5700ac4..58a2bb323 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -5,6 +5,20 @@ #include #include +#include + +inline const std::vector>> + HOTKEY_ARG_ON_OFF_TOGGLE = { + {"Toggle", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, +}; + +inline const std::vector>> + HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION = { + {"No", {"withoutSelection"}}, + {"Yes", {"withSelection"}}, +}; namespace chatterino { @@ -13,6 +27,9 @@ struct ActionDefinition { // displayName is the value that would be shown to a user when they edit or create a hotkey for an action QString displayName; + // argumentDescription is a description of the arguments in a format of + // " [optional arg: possible + // values]" QString argumentDescription = ""; // minCountArguments is the minimum amount of arguments the action accepts @@ -21,6 +38,20 @@ struct ActionDefinition { // maxCountArguments is the maximum amount of arguments the action accepts uint8_t maxCountArguments = minCountArguments; + + // possibleArguments is empty or contains all possible argument values, + // it is an ordered mapping from option name (what the user sees) to + // arguments (what the action code will see). + // As std::map does not guarantee order this is a std::vector<...> + std::vector>> possibleArguments = + {}; + + // When possibleArguments are present this should be a string like + // "Direction:" which will be shown before the values from + // possibleArguments in the UI. Otherwise, it should be empty. + QString argumentsPrompt = ""; + // A more detailed description of what argumentsPrompt means + QString argumentsPromptHover = ""; }; using ActionDefinitionMap = std::map; @@ -39,9 +70,15 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", }}, {"search", ActionDefinition{"Focus search box"}}, {"execModeratorAction", @@ -57,9 +94,19 @@ inline const std::map actionNames{ {"delete", ActionDefinition{"Close"}}, {"focus", ActionDefinition{ - "Focus neighbouring split", - "", - 1, + .displayName = "Focus neighbouring split", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + {"Left", {"left"}}, + {"Right", {"right"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction to look for a split to focus?", }}, {"openInBrowser", ActionDefinition{"Open channel in browser"}}, {"openInCustomPlayer", @@ -71,10 +118,18 @@ inline const std::map actionNames{ {"reconnect", ActionDefinition{"Reconnect to chat"}}, {"reloadEmotes", ActionDefinition{ - "Reload emotes", - "[channel or subscriber]", - 0, - 1, + .displayName = "Reload emotes", + .argumentDescription = + "[type: channel or subscriber; default: all emotes]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"All emotes", {}}, + {"Channel emotes only", {"channel"}}, + {"Subscriber emotes only", {"subscriber"}}, + }, + .argumentsPrompt = "Emote type:", + .argumentsPromptHover = "Which emotes should Chatterino reload", }}, {"runCommand", ActionDefinition{ @@ -84,25 +139,41 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction do you want to see more messages", }}, {"scrollToBottom", ActionDefinition{"Scroll to the bottom"}}, {"scrollToTop", ActionDefinition{"Scroll to the top"}}, {"setChannelNotification", ActionDefinition{ - "Set channel live notification", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set channel live notification", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = "Should the channel live notification be " + "enabled, disabled or toggled", }}, {"setModerationMode", ActionDefinition{ - "Set moderation mode", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set moderation mode", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should the moderation mode be enabled, disabled or toggled", }}, {"showSearch", ActionDefinition{"Search current channel"}}, {"showGlobalSearch", ActionDefinition{"Search all channels"}}, @@ -114,21 +185,38 @@ inline const std::map actionNames{ {"clear", ActionDefinition{"Clear message"}}, {"copy", ActionDefinition{ - "Copy", - "", - 1, + .displayName = "Copy", + .argumentDescription = + "", + .minCountArguments = 1, + .possibleArguments{ + {"Automatic", {"auto"}}, + {"Split", {"split"}}, + {"Split Input", {"splitInput"}}, + }, + .argumentsPrompt = "Source of text:", }}, {"cursorToStart", ActionDefinition{ - "To start of message", - "", - 1, + .displayName = "To start of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to start:", + // XXX: write a hover for this that doesn't suck }}, {"cursorToEnd", ActionDefinition{ - "To end of message", - "", - 1, + .displayName = "To end of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to end:", + // XXX: write a hover for this that doesn't suck }}, {"nextMessage", ActionDefinition{"Choose next sent message"}}, {"openEmotesPopup", ActionDefinition{"Open emotes list"}}, @@ -140,10 +228,16 @@ inline const std::map actionNames{ {"selectWord", ActionDefinition{"Select word"}}, {"sendMessage", ActionDefinition{ - "Send message", - "[keepInput to not clear the text after sending]", - 0, - 1, + .displayName = "Send message", + .argumentDescription = + "[keepInput to not clear the text after sending]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"Default behavior", {}}, + {"Keep message in input after sending it", {"keepInput"}}, + }, + .argumentsPrompt = "Behavior:", }}, {"undo", ActionDefinition{"Undo"}}, @@ -163,7 +257,7 @@ inline const std::map actionNames{ {"moveTab", ActionDefinition{ "Move tab", - "", + "", 1, }}, {"newSplit", ActionDefinition{"Create a new split"}}, @@ -172,40 +266,73 @@ inline const std::map actionNames{ {"openTab", ActionDefinition{ "Select tab", - "", + "", 1, }}, {"openQuickSwitcher", ActionDefinition{"Open the quick switcher"}}, {"popup", ActionDefinition{ - "New popup", - "", - 1, + .displayName = "New popup", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Focused Split", {"split"}}, + {"Entire Tab", {"window"}}, + }, + .argumentsPrompt = "Include:", + .argumentsPromptHover = + "What should be included in the new popup", }}, {"quit", ActionDefinition{"Quit Chatterino"}}, {"removeTab", ActionDefinition{"Remove current tab"}}, {"reopenSplit", ActionDefinition{"Reopen closed split"}}, {"setStreamerMode", ActionDefinition{ - "Set streamer mode", - "[on, off, toggle, or auto. default: toggle]", - 0, - 1, + .displayName = "Set streamer mode", + .argumentDescription = + "[on, off, toggle, or auto. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = + { + {"Toggle on/off", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, + {"Set to automatic", {"auto"}}, + }, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should streamer mode be enabled, disabled, toggled (on/off) " + "or set to auto", }}, {"toggleLocalR9K", ActionDefinition{"Toggle local R9K"}}, {"zoom", ActionDefinition{ - "Zoom in/out", - "", - 1, + .displayName = "Zoom in/out", + .argumentDescription = "Argument:", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = + { + {"Zoom in", {"in"}}, + {"Zoom out", {"out"}}, + {"Reset zoom", {"reset"}}, + }, + .argumentsPrompt = "Option:", }}, {"setTabVisibility", ActionDefinition{ - "Set tab visibility", - "[on, off, or toggle. default: toggle]", - 0, - 1, - }}}}, + .displayName = "Set tab visibility", + .argumentDescription = "[on, off, or toggle. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should the tabs be enabled, disabled or toggled.", + }}, + }}, }; } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.cpp b/src/controllers/hotkeys/HotkeyHelpers.cpp index d998d7665..2859ad6d2 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.cpp +++ b/src/controllers/hotkeys/HotkeyHelpers.cpp @@ -1,5 +1,9 @@ #include "controllers/hotkeys/HotkeyHelpers.hpp" +#include "controllers/hotkeys/ActionNames.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" + +#include #include namespace chatterino { @@ -27,4 +31,20 @@ std::vector parseHotkeyArguments(QString argumentString) return arguments; } +boost::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action) +{ + auto allActions = actionNames.find(category); + if (allActions != actionNames.end()) + { + const auto &actionsMap = allActions->second; + auto definition = actionsMap.find(action); + if (definition != actionsMap.end()) + { + return {definition->second}; + } + } + return {}; +} + } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.hpp b/src/controllers/hotkeys/HotkeyHelpers.hpp index 4e63569ff..dfbdb6f2d 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.hpp +++ b/src/controllers/hotkeys/HotkeyHelpers.hpp @@ -1,5 +1,8 @@ #pragma once +#include "controllers/hotkeys/ActionNames.hpp" + +#include #include #include @@ -7,5 +10,7 @@ namespace chatterino { std::vector parseHotkeyArguments(QString argumentString); +boost::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action); } // namespace chatterino diff --git a/src/widgets/dialogs/EditHotkeyDialog.cpp b/src/widgets/dialogs/EditHotkeyDialog.cpp index d1bac90cc..e0fe23291 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.cpp +++ b/src/widgets/dialogs/EditHotkeyDialog.cpp @@ -17,6 +17,14 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, , data_(hotkey) { this->ui_->setupUi(this); + this->setStyleSheet(R"(QToolTip { + padding: 2px; + background-color: #333333; + border: 1px solid #545454; + color: white; +})"); + this->ui_->easyArgsPicker->setVisible(false); + this->ui_->easyArgsLabel->setVisible(false); // dynamically add category names to the category picker for (const auto &[_, hotkeyCategory] : getApp()->hotkeys->categories()) { @@ -28,34 +36,7 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, if (hotkey) { - if (!hotkey->validAction()) - { - this->showEditError("Invalid action, make sure you select the " - "correct action before saving."); - } - - // editing a hotkey - - // update pickers/input boxes to values from Hotkey object - this->ui_->categoryPicker->setCurrentIndex(size_t(hotkey->category())); - this->ui_->keyComboEdit->setKeySequence( - QKeySequence::fromString(hotkey->keySequence().toString())); - this->ui_->nameEdit->setText(hotkey->name()); - // update arguments - QString argsText; - bool first = true; - for (const auto &arg : hotkey->arguments()) - { - if (!first) - { - argsText += '\n'; - } - - argsText += arg; - - first = false; - } - this->ui_->argumentsEdit->setPlainText(argsText); + this->setFromHotkey(hotkey); } else { @@ -66,6 +47,96 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, this->ui_->argumentsEdit->setPlainText(""); } } +void EditHotkeyDialog::setFromHotkey(std::shared_ptr hotkey) +{ + if (!hotkey->validAction()) + { + this->showEditError("Invalid action, make sure you select the " + "correct action before saving."); + } + + // editing a hotkey + + // update pickers/input boxes to values from Hotkey object + this->ui_->categoryPicker->setCurrentIndex(size_t(hotkey->category())); + this->ui_->keyComboEdit->setKeySequence( + QKeySequence::fromString(hotkey->keySequence().toString())); + this->ui_->nameEdit->setText(hotkey->name()); + + auto def = findHotkeyActionDefinition(hotkey->category(), hotkey->action()); + if (def.has_value() && !def->possibleArguments.empty()) + { + qCDebug(chatterinoHotkeys) << "Enabled easy picker and arg edit " + "because we have arguments from hotkey"; + this->ui_->easyArgsLabel->setVisible(true); + this->ui_->easyArgsPicker->setVisible(true); + + this->ui_->argumentsEdit->setVisible(false); + this->ui_->argumentsLabel->setVisible(false); + this->ui_->argumentsDescription->setVisible(false); + + this->ui_->easyArgsPicker->clear(); + this->ui_->easyArgsLabel->setText(def->argumentsPrompt); + this->ui_->easyArgsLabel->setToolTip(def->argumentsPromptHover); + int matchIdx = -1; + for (int i = 0; i < def->possibleArguments.size(); i++) + { + const auto &[displayText, argData] = def->possibleArguments.at(i); + this->ui_->easyArgsPicker->addItem(displayText); + + // check if matches + if (argData.size() != hotkey->arguments().size()) + { + continue; + } + bool matches = true; + for (int j = 0; j < argData.size(); j++) + { + if (argData.at(j) != hotkey->arguments().at(j)) + { + matches = false; + break; + } + } + if (matches) + { + matchIdx = i; + } + } + if (matchIdx != -1) + { + this->ui_->easyArgsPicker->setCurrentIndex(matchIdx); + return; + } + + qCDebug(chatterinoHotkeys) + << "Did not match hotkey arguments for " << hotkey->toString() + << "using text edit instead of easy picker"; + this->showEditError("Arguments do not match what's expected. The " + "argument picker is not available."); + this->ui_->easyArgsLabel->setVisible(false); + this->ui_->easyArgsPicker->setVisible(false); + + this->ui_->argumentsEdit->setVisible(true); + this->ui_->argumentsLabel->setVisible(true); + this->ui_->argumentsDescription->setVisible(true); + } + // update arguments + QString argsText; + bool first = true; + for (const auto &arg : hotkey->arguments()) + { + if (!first) + { + argsText += '\n'; + } + + argsText += arg; + + first = false; + } + this->ui_->argumentsEdit->setPlainText(argsText); +} EditHotkeyDialog::~EditHotkeyDialog() { @@ -151,6 +222,14 @@ void EditHotkeyDialog::afterEdit() action = actionTemp.toString(); } + auto def = findHotkeyActionDefinition(*category, action); + if (def.has_value() && this->ui_->easyArgsPicker->isVisible()) + { + arguments = + def->possibleArguments.at(this->ui_->easyArgsPicker->currentIndex()) + .second; + } + auto hotkey = std::make_shared( *category, this->ui_->keyComboEdit->keySequence(), action, arguments, nameText); @@ -263,44 +342,69 @@ void EditHotkeyDialog::updateArgumentsInput() } const ActionDefinition &def = definition->second; - if (def.maxCountArguments != 0) - { - QString text = - "Arguments wrapped in <> are required.\nArguments wrapped in " - "[] " - "are optional.\nArguments are separated by a newline."; - if (!def.argumentDescription.isEmpty()) - { - this->ui_->argumentsDescription->setVisible(true); - this->ui_->argumentsDescription->setText( - def.argumentDescription); - } - else - { - this->ui_->argumentsDescription->setVisible(false); - } - - text = QString("Arguments wrapped in <> are required."); - if (def.maxCountArguments != def.minCountArguments) - { - text += QString("\nArguments wrapped in [] are optional."); - } - - text += "\nArguments are separated by a newline."; - - this->ui_->argumentsEdit->setEnabled(true); - this->ui_->argumentsEdit->setPlaceholderText(text); - - this->ui_->argumentsLabel->setVisible(true); - this->ui_->argumentsDescription->setVisible(true); - this->ui_->argumentsEdit->setVisible(true); - } - else + if (def.maxCountArguments == 0) { + qCDebug(chatterinoHotkeys) << "Disabled easy picker and arg edit " + "because we don't have any arguments"; this->ui_->argumentsLabel->setVisible(false); this->ui_->argumentsDescription->setVisible(false); this->ui_->argumentsEdit->setVisible(false); + + this->ui_->easyArgsLabel->setVisible(false); + this->ui_->easyArgsPicker->setVisible(false); + return; } + if (!def.argumentDescription.isEmpty()) + { + this->ui_->argumentsDescription->setVisible(true); + this->ui_->argumentsDescription->setText(def.argumentDescription); + } + else + { + this->ui_->argumentsDescription->setVisible(false); + } + + QString text = "Arguments wrapped in <> are required."; + if (def.maxCountArguments != def.minCountArguments) + { + text += QString("\nArguments wrapped in [] are optional."); + } + + text += "\nArguments are separated by a newline."; + + this->ui_->argumentsEdit->setEnabled(true); + this->ui_->argumentsEdit->setPlaceholderText(text); + + this->ui_->argumentsLabel->setVisible(true); + this->ui_->argumentsDescription->setVisible(true); + this->ui_->argumentsEdit->setVisible(true); + + // update easy picker + if (def.possibleArguments.empty()) + { + qCDebug(chatterinoHotkeys) + << "Disabled easy picker because we have possible arguments"; + this->ui_->easyArgsPicker->setVisible(false); + this->ui_->easyArgsLabel->setVisible(false); + return; + } + qCDebug(chatterinoHotkeys) + << "Enabled easy picker because we have possible arguments"; + this->ui_->easyArgsPicker->setVisible(true); + this->ui_->easyArgsLabel->setVisible(true); + + this->ui_->argumentsLabel->setVisible(false); + this->ui_->argumentsEdit->setVisible(false); + this->ui_->argumentsDescription->setVisible(false); + + this->ui_->easyArgsPicker->clear(); + for (const auto &[displayText, _] : def.possibleArguments) + { + this->ui_->easyArgsPicker->addItem(displayText); + } + this->ui_->easyArgsPicker->setCurrentIndex(0); + this->ui_->easyArgsLabel->setText(def.argumentsPrompt); + this->ui_->easyArgsLabel->setToolTip(def.argumentsPromptHover); } } diff --git a/src/widgets/dialogs/EditHotkeyDialog.hpp b/src/widgets/dialogs/EditHotkeyDialog.hpp index d2c1f5e85..3f8bed158 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.hpp +++ b/src/widgets/dialogs/EditHotkeyDialog.hpp @@ -49,6 +49,7 @@ protected slots: private: void showEditError(QString errorText); + void setFromHotkey(std::shared_ptr hotkey); Ui::EditHotkeyDialog *ui_; std::shared_ptr data_; diff --git a/src/widgets/dialogs/EditHotkeyDialog.ui b/src/widgets/dialogs/EditHotkeyDialog.ui index d7f265b0d..7ddb8a21d 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.ui +++ b/src/widgets/dialogs/EditHotkeyDialog.ui @@ -7,7 +7,7 @@ 0 0 400 - 300 + 400 @@ -42,6 +42,9 @@ see this message :) + + Set a name for the hotkey so you will be able to identify it later + Name: @@ -76,6 +79,9 @@ see this message :) + + + @@ -95,6 +101,9 @@ see this message :) + + Pressing this keybinding will invoke the hotkey + Keybinding: @@ -107,6 +116,16 @@ see this message :) + + + You are not supposed to see this, please report this! + + + Argument: + + + + Arguments: @@ -116,7 +135,7 @@ see this message :) - + You should never see this message :) @@ -126,7 +145,7 @@ see this message :) - + @@ -136,8 +155,18 @@ see this message :) - - + + + + + 0 + 0 + + + + + + @@ -169,8 +198,8 @@ see this message :) afterEdit() - 257 - 290 + 263 + 352 157 @@ -185,8 +214,8 @@ see this message :) reject() - 325 - 290 + 331 + 352 286 @@ -201,8 +230,8 @@ see this message :) updatePossibleActions() - 246 - 85 + 172 + 118 75 @@ -217,8 +246,8 @@ see this message :) updateArgumentsInput() - 148 - 119 + 172 + 156 74 From e9432d3b6590090961b869fdc260d3ec462ada39 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 29 May 2023 16:37:55 +0200 Subject: [PATCH 22/83] Clean up GenericListModel (#4661) Co-authored-by: Daniel Sage --- src/widgets/listview/GenericListModel.cpp | 21 ++++++++++++++++----- src/widgets/listview/GenericListModel.hpp | 16 +++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/widgets/listview/GenericListModel.cpp b/src/widgets/listview/GenericListModel.cpp index 9163b2b31..dc98fb827 100644 --- a/src/widgets/listview/GenericListModel.cpp +++ b/src/widgets/listview/GenericListModel.cpp @@ -2,12 +2,12 @@ namespace chatterino { -GenericListModel::GenericListModel(QWidget *parent) +GenericListModel::GenericListModel(QObject *parent) : QAbstractListModel(parent) { } -int GenericListModel::rowCount(const QModelIndex &parent) const +int GenericListModel::rowCount(const QModelIndex & /*parent*/) const { return this->items_.size(); } @@ -15,12 +15,16 @@ int GenericListModel::rowCount(const QModelIndex &parent) const QVariant GenericListModel::data(const QModelIndex &index, int /* role */) const { if (!index.isValid()) - return QVariant(); + { + return {}; + } if (index.row() >= this->items_.size()) - return QVariant(); + { + return {}; + } - auto item = this->items_[index.row()].get(); + auto *item = this->items_[index.row()].get(); // See https://stackoverflow.com/a/44503822 . return QVariant::fromValue(static_cast(item)); } @@ -37,7 +41,9 @@ void GenericListModel::addItem(std::unique_ptr item) void GenericListModel::clear() { if (this->items_.empty()) + { return; + } // {begin,end}RemoveRows needs to be called to notify attached views this->beginRemoveRows(QModelIndex(), 0, this->items_.size() - 1); @@ -48,4 +54,9 @@ void GenericListModel::clear() this->endRemoveRows(); } +void GenericListModel::reserve(size_t capacity) +{ + this->items_.reserve(capacity); +} + } // namespace chatterino diff --git a/src/widgets/listview/GenericListModel.hpp b/src/widgets/listview/GenericListModel.hpp index 0f2fe2175..10106730b 100644 --- a/src/widgets/listview/GenericListModel.hpp +++ b/src/widgets/listview/GenericListModel.hpp @@ -3,23 +3,24 @@ #include "widgets/listview/GenericListItem.hpp" #include -#include +#include #include +#include namespace chatterino { class GenericListModel : public QAbstractListModel { public: - GenericListModel(QWidget *parent = nullptr); + GenericListModel(QObject *parent = nullptr); /** * @brief Reimplements QAbstractItemModel::rowCount. * - * @return number of items currrently present in this model + * @return number of items currently present in this model */ - int rowCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; /** * @brief Reimplements QAbstractItemModel::data. Currently, the role parameter @@ -30,7 +31,7 @@ public: * * @return GenericListItem * (wrapped as QVariant) at index */ - QVariant data(const QModelIndex &index, int role) const; + QVariant data(const QModelIndex &index, int role) const override; /** * @brief Add an item to this QuickSwitcherModel. It will be displayed in @@ -50,6 +51,11 @@ public: */ void clear(); + /** + * @brief Increases the capacity of the list model. + */ + void reserve(size_t capacity); + private: std::vector> items_; }; From bf4148a3701d1b29662938eb6179556d93fcd3b1 Mon Sep 17 00:00:00 2001 From: chrrs <9766338+chrrs@users.noreply.github.com> Date: Wed, 31 May 2023 21:38:17 +0200 Subject: [PATCH 23/83] Consider nicknames when searching for messages (#4663) Co-authored-by: pajlada --- CHANGELOG.md | 1 + src/controllers/nicknames/Nickname.hpp | 16 ++++++++-------- src/messages/SharedMessageBuilder.cpp | 9 ++------- src/providers/irc/IrcMessageBuilder.cpp | 6 +++++- src/providers/twitch/TwitchMessageBuilder.cpp | 6 +++++- src/singletons/Settings.cpp | 16 ++++++++++++++++ src/singletons/Settings.hpp | 1 + src/widgets/settingspages/NicknamesPage.cpp | 2 +- 8 files changed, 39 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68eadc707..4e8332da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Nicknames are now taken into consideration when searching for messages. (#4663) - Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Minor: Improved editing hotkeys. (#4628) diff --git a/src/controllers/nicknames/Nickname.hpp b/src/controllers/nicknames/Nickname.hpp index 529f5fa0d..fa8bdf6d0 100644 --- a/src/controllers/nicknames/Nickname.hpp +++ b/src/controllers/nicknames/Nickname.hpp @@ -3,6 +3,7 @@ #include "util/RapidjsonHelpers.hpp" #include "util/RapidJsonSerializeQString.hpp" +#include #include #include #include @@ -58,25 +59,25 @@ public: return this->isCaseSensitive_; } - [[nodiscard]] bool match(QString &usernameText) const + [[nodiscard]] boost::optional match( + const QString &usernameText) const { if (this->isRegex()) { if (!this->regex_.isValid()) { - return false; + return boost::none; } if (this->name().isEmpty()) { - return false; + return boost::none; } auto workingCopy = usernameText; workingCopy.replace(this->regex_, this->replace()); if (workingCopy != usernameText) { - usernameText = workingCopy; - return true; + return workingCopy; } } else @@ -85,12 +86,11 @@ public: this->name().compare(usernameText, this->caseSensitivity()); if (res == 0) { - usernameText = this->replace(); - return true; + return this->replace(); } } - return false; + return boost::none; } private: diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index 9d0fda90c..97c795c3a 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -270,14 +270,9 @@ QString SharedMessageBuilder::stylizeUsername(const QString &username, break; } - auto nicknames = getCSettings().nicknames.readOnly(); - - for (const auto &nickname : *nicknames) + if (auto nicknameText = getCSettings().matchNickname(usernameText)) { - if (nickname.match(usernameText)) - { - break; - } + usernameText = *nicknameText; } return usernameText; diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp index 0474bdb76..3b7fb30da 100644 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ b/src/providers/irc/IrcMessageBuilder.cpp @@ -64,7 +64,11 @@ MessagePtr IrcMessageBuilder::build() // message this->addIrcMessageText(this->originalMessage_); - this->message().searchText = this->message().localizedName + " " + + QString stylizedUsername = + this->stylizeUsername(this->userName, this->message()); + + this->message().searchText = stylizedUsername + " " + + this->message().localizedName + " " + this->userName + ": " + this->originalMessage_; // highlights diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 98888fa48..3cd93303c 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -294,8 +294,12 @@ MessagePtr TwitchMessageBuilder::build() this->addWords(splits, twitchEmotes); + QString stylizedUsername = + this->stylizeUsername(this->userName, this->message()); + this->message().messageText = this->originalMessage_; - this->message().searchText = this->message().localizedName + " " + + this->message().searchText = stylizedUsername + " " + + this->message().localizedName + " " + this->userName + ": " + this->originalMessage_; // highlights diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 07e10142c..e01f316a1 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -81,6 +81,22 @@ bool ConcurrentSettings::isMutedChannel(const QString &channelName) return false; } +boost::optional ConcurrentSettings::matchNickname( + const QString &usernameText) +{ + auto nicknames = getCSettings().nicknames.readOnly(); + + for (const auto &nickname : *nicknames) + { + if (auto nicknameText = nickname.match(usernameText)) + { + return nicknameText; + } + } + + return boost::none; +} + void ConcurrentSettings::mute(const QString &channelName) { mutedChannels.append(channelName); diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 4ff10a071..3d393a6ab 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -47,6 +47,7 @@ public: bool isBlacklistedUser(const QString &username); bool isMutedChannel(const QString &channelName); bool toggleMutedChannel(const QString &channelName); + boost::optional matchNickname(const QString &username); private: void mute(const QString &channelName); diff --git a/src/widgets/settingspages/NicknamesPage.cpp b/src/widgets/settingspages/NicknamesPage.cpp index e8fcf56fe..a61042bf9 100644 --- a/src/widgets/settingspages/NicknamesPage.cpp +++ b/src/widgets/settingspages/NicknamesPage.cpp @@ -18,7 +18,7 @@ NicknamesPage::NicknamesPage() auto layout = layoutCreator.setLayoutType(); layout.emplace( - "Nicknames do not work with features such as search or user highlights." + "Nicknames do not work with features such as user highlights." "\nWith those features you will still need to use the user's original " "name."); EditableModelView *view = From d0299984ed61a3a3dcf604c5a98903157d66c5bd Mon Sep 17 00:00:00 2001 From: chrrs <9766338+chrrs@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:09:02 +0200 Subject: [PATCH 24/83] Add 'chrrs' to contributors.txt (#4664) --- resources/contributors.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/contributors.txt b/resources/contributors.txt index 2a8be7985..7ad340e10 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -63,6 +63,7 @@ Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor 2547techno | https://github.com/2547techno | :/avatars/techno.png | Contributor ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor olafyang | https://github.com/olafyang | | Contributor +chrrs | https://github.com/chrrs | | Contributor # If you are a contributor add yourself above this line From e803b6de95e314556ba462e75922006e150b85a3 Mon Sep 17 00:00:00 2001 From: Wissididom <30803034+Wissididom@users.noreply.github.com> Date: Sun, 4 Jun 2023 11:40:11 +0200 Subject: [PATCH 25/83] Remove duplicate Fluent icons license (#4665) --- CHANGELOG.md | 1 + src/widgets/settingspages/AboutPage.cpp | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8332da0..d78083151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) - Dev: Expanded upon `$$$` test channels. (#4655) - Dev: Added tools to help debug image GC. (#4578) +- Dev: Removed duplicate license when having plugins enabled. (#4665) ## 2.4.4 diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 9ea3153b9..affea98cd 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -117,9 +117,6 @@ AboutPage::AboutPage() #ifdef CHATTERINO_HAVE_PLUGINS addLicense(form.getElement(), "lua", "https://lua.org", ":/licenses/lua.txt"); - addLicense(form.getElement(), "Fluent icons", - "https://github.com/microsoft/fluentui-system-icons", - ":/licenses/fluenticons.txt"); #endif #ifdef CHATTERINO_WITH_CRASHPAD addLicense(form.getElement(), "sentry-crashpad", From 6681ed5bfb5c798c3141d0d51828509d5ac50404 Mon Sep 17 00:00:00 2001 From: Arne <78976058+4rneee@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:24:04 +0200 Subject: [PATCH 26/83] Remove QObjectRef in favor of QPointer (#4666) * replace usage of QObjectRef with QPointer * delete QObjectRef class * inlucde QPointer header * Add changelog entry * use isNull() instead of ! data() --------- Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/common/NetworkPrivate.cpp | 10 ++-- src/common/NetworkPrivate.hpp | 4 +- src/providers/irc/IrcServer.cpp | 4 +- src/util/QObjectRef.hpp | 86 ------------------------------- src/widgets/splits/SplitInput.cpp | 14 ++--- src/widgets/splits/SplitInput.hpp | 6 +-- 7 files changed, 20 insertions(+), 105 deletions(-) delete mode 100644 src/util/QObjectRef.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d78083151..9387dd41e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Dev: Expanded upon `$$$` test channels. (#4655) - Dev: Added tools to help debug image GC. (#4578) - Dev: Removed duplicate license when having plugins enabled. (#4665) +- Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666) ## 2.4.4 diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index f0a74ed5a..9af301883 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -174,7 +174,7 @@ void loadUncached(std::shared_ptr &&data) } auto handleReply = [data, reply]() mutable { - if (data->hasCaller_ && !data->caller_.get()) + if (data->hasCaller_ && data->caller_.isNull()) { return; } @@ -350,7 +350,7 @@ void loadCached(std::shared_ptr &&data) // XXX: If outcome is Failure, we should invalidate the cache file // somehow/somewhere /*auto outcome =*/ - if (data->hasCaller_ && !data->caller_.get()) + if (data->hasCaller_ && data->caller_.isNull()) { return; } @@ -359,7 +359,7 @@ void loadCached(std::shared_ptr &&data) else { postToThread([data, result]() { - if (data->hasCaller_ && !data->caller_.get()) + if (data->hasCaller_ && data->caller_.isNull()) { return; } @@ -373,7 +373,7 @@ void loadCached(std::shared_ptr &&data) { if (data->executeConcurrently_ || isGuiThread()) { - if (data->hasCaller_ && !data->caller_.get()) + if (data->hasCaller_ && data->caller_.isNull()) { return; } @@ -383,7 +383,7 @@ void loadCached(std::shared_ptr &&data) else { postToThread([data]() { - if (data->hasCaller_ && !data->caller_.get()) + if (data->hasCaller_ && data->caller_.isNull()) { return; } diff --git a/src/common/NetworkPrivate.hpp b/src/common/NetworkPrivate.hpp index 3fe841bc2..d48000aeb 100644 --- a/src/common/NetworkPrivate.hpp +++ b/src/common/NetworkPrivate.hpp @@ -1,10 +1,10 @@ #pragma once #include "common/NetworkCommon.hpp" -#include "util/QObjectRef.hpp" #include #include +#include #include #include @@ -38,7 +38,7 @@ struct NetworkData { QNetworkRequest request_; bool hasCaller_{}; - QObjectRef caller_; + QPointer caller_; bool cache_{}; bool executeConcurrently_{}; diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 5ae01c56b..ad94d0305 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -11,9 +11,9 @@ #include "providers/twitch/TwitchIrcServer.hpp" // NOTE: Included to access the mentions channel #include "singletons/Settings.hpp" #include "util/IrcHelpers.hpp" -#include "util/QObjectRef.hpp" #include +#include #include #include @@ -151,7 +151,7 @@ void IrcServer::initializeConnection(IrcConnection *connection, [[fallthrough]]; case IrcAuthType::Pass: this->data_->getPassword( - this, [conn = new QObjectRef(connection) /* can't copy */, + this, [conn = new QPointer(connection) /* can't copy */, this](const QString &password) mutable { if (*conn) { diff --git a/src/util/QObjectRef.hpp b/src/util/QObjectRef.hpp deleted file mode 100644 index 07444d085..000000000 --- a/src/util/QObjectRef.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace chatterino { -/// Holds a pointer to a QObject and resets it to nullptr if the QObject -/// gets destroyed. -template -class QObjectRef -{ -public: - QObjectRef() - { - static_assert(std::is_base_of_v); - } - - explicit QObjectRef(T *t) - { - static_assert(std::is_base_of_v); - - this->set(t); - } - - QObjectRef(const QObjectRef &other) - { - this->set(other.t_); - } - - ~QObjectRef() - { - this->set(nullptr); - } - - QObjectRef &operator=(T *t) - { - this->set(t); - - return *this; - } - - operator bool() - { - return t_; - } - - T *operator->() - { - return t_; - } - - T *get() - { - return t_; - } - -private: - void set(T *other) - { - // old - if (this->conn_) - { - QObject::disconnect(this->conn_); - } - - // new - if (other) - { - // the cast here should absolutely not be necessary, but gcc still requires it - this->conn_ = - QObject::connect((QObject *)other, &QObject::destroyed, qApp, - [this](QObject *) { - this->set(nullptr); - }, - Qt::DirectConnection); - } - - this->t_ = other; - } - - std::atomic t_{}; - QMetaObject::Connection conn_; -}; -} // namespace chatterino diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index 56af3e5d5..c76e48f63 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -630,7 +630,7 @@ bool SplitInput::eventFilter(QObject *obj, QEvent *event) if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::Shortcut) { - if (auto popup = this->inputCompletionPopup_.get()) + if (auto popup = this->inputCompletionPopup_.data()) { if (popup->isVisible()) { @@ -650,7 +650,7 @@ void SplitInput::installKeyPressedEvent() { this->ui_.textEdit->keyPressed.disconnectAll(); this->ui_.textEdit->keyPressed.connect([this](QKeyEvent *event) { - if (auto *popup = this->inputCompletionPopup_.get()) + if (auto *popup = this->inputCompletionPopup_.data()) { if (popup->isVisible()) { @@ -764,12 +764,12 @@ void SplitInput::updateCompletionPopup() void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion) { - if (!this->inputCompletionPopup_.get()) + if (this->inputCompletionPopup_.isNull()) { this->inputCompletionPopup_ = new InputCompletionPopup(this); this->inputCompletionPopup_->setInputAction( - [that = QObjectRef(this)](const QString &text) mutable { - if (auto *this2 = that.get()) + [that = QPointer(this)](const QString &text) mutable { + if (auto *this2 = that.data()) { this2->insertCompletionText(text); this2->hideCompletionPopup(); @@ -777,7 +777,7 @@ void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion) }); } - auto *popup = this->inputCompletionPopup_.get(); + auto *popup = this->inputCompletionPopup_.data(); assert(popup); if (emoteCompletion) @@ -798,7 +798,7 @@ void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion) void SplitInput::hideCompletionPopup() { - if (auto *popup = this->inputCompletionPopup_.get()) + if (auto *popup = this->inputCompletionPopup_.data()) { popup->hide(); } diff --git a/src/widgets/splits/SplitInput.hpp b/src/widgets/splits/SplitInput.hpp index 2199f855d..192795c73 100644 --- a/src/widgets/splits/SplitInput.hpp +++ b/src/widgets/splits/SplitInput.hpp @@ -1,12 +1,12 @@ #pragma once -#include "util/QObjectRef.hpp" #include "widgets/BaseWidget.hpp" #include #include #include #include +#include #include #include #include @@ -113,8 +113,8 @@ protected: Split *const split_; ChannelView *const channelView_; - QObjectRef emotePopup_; - QObjectRef inputCompletionPopup_; + QPointer emotePopup_; + QPointer inputCompletionPopup_; struct { ResizingTextEdit *textEdit; From f306e288b900dd40ed23a688cfb4bdf171d14784 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 4 Jun 2023 14:21:16 +0200 Subject: [PATCH 27/83] Prevent Generation of Crashdumps When the Browser Is Closed (#4667) * fix: extension process generating crashdumps * fix: move `getApp` call * chore: remove distracting comments * chore: add changelog entry --- CHANGELOG.md | 1 + src/BrowserExtension.cpp | 1 + src/singletons/NativeMessaging.cpp | 6 ++---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9387dd41e..5d3d2090a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Minor: Improved editing hotkeys. (#4628) +- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index dad0ac2af..8ae25ac99 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -75,6 +75,7 @@ namespace { client.sendMessage(data); } + _Exit(0); } } // namespace diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 620e5ee37..5e72e131f 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -189,7 +189,6 @@ void NativeMessagingServer::ReceiverThread::run() void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { - auto app = getApp(); QString action = root.value("action").toString(); if (action.isNull()) @@ -237,6 +236,8 @@ void NativeMessagingServer::ReceiverThread::handleMessage( if (_type == "twitch") { postToThread([=] { + auto *app = getApp(); + if (!name.isEmpty()) { auto channel = app->twitch->getOrAddChannel(name); @@ -249,15 +250,12 @@ void NativeMessagingServer::ReceiverThread::handleMessage( if (attach || attachFullscreen) { #ifdef USEWINSDK - // if (args.height != -1) { auto *window = AttachedWindow::get(::GetForegroundWindow(), args); if (!name.isEmpty()) { window->setChannel(app->twitch->getOrAddChannel(name)); } -// } -// window->show(); #endif } }); From ca9c91a15b550afab2eb28087b45c5b745679eda Mon Sep 17 00:00:00 2001 From: Arne <78976058+4rneee@users.noreply.github.com> Date: Sun, 4 Jun 2023 19:01:37 +0200 Subject: [PATCH 28/83] Add 4rneee to contributors list (#4668) --- resources/contributors.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/contributors.txt b/resources/contributors.txt index 7ad340e10..b2167359d 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -64,6 +64,7 @@ Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor olafyang | https://github.com/olafyang | | Contributor chrrs | https://github.com/chrrs | | Contributor +4rneee | https://github.com/4rneee | | Contributor # If you are a contributor add yourself above this line From f0c4eb7caad5535ddf295e078300915670153e73 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 10 Jun 2023 12:11:05 +0200 Subject: [PATCH 29/83] Fix undefined behaviour when loading non-existant credentials (#4674) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/common/Credentials.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d3d2090a..eaeb94f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Dev: Added tools to help debug image GC. (#4578) - Dev: Removed duplicate license when having plugins enabled. (#4665) - Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666) +- Dev: Fixed undefined behavior when loading non-existant credentials. (#4673) ## 2.4.4 diff --git a/src/common/Credentials.cpp b/src/common/Credentials.cpp index f3985701f..d3bb5ebbd 100644 --- a/src/common/Credentials.cpp +++ b/src/common/Credentials.cpp @@ -197,9 +197,9 @@ void Credentials::get(const QString &provider, const QString &name_, } else { - auto &instance = insecureInstance(); + const auto &instance = insecureInstance(); - onLoaded(instance.object().find(name).value().toString()); + onLoaded(instance[name].toString()); } } From 335dff53af63d798db70921c5b09bb230849da33 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 10 Jun 2023 12:55:47 +0200 Subject: [PATCH 30/83] Don't add QLayouts to QWidgets that already have one (#4672) --- CHANGELOG.md | 1 + src/widgets/Window.cpp | 2 +- src/widgets/dialogs/EmotePopup.cpp | 4 ++-- src/widgets/helper/EditableModelView.cpp | 2 +- src/widgets/helper/SearchPopup.cpp | 2 +- src/widgets/splits/Split.cpp | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaeb94f58..285c736ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Dev: Added tools to help debug image GC. (#4578) - Dev: Removed duplicate license when having plugins enabled. (#4665) - Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666) +- Dev: Fixed warnings about QWidgets already having a QLayout. (#4672) - Dev: Fixed undefined behavior when loading non-existant credentials. (#4673) ## 2.4.4 diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 12f605838..efcd33d4e 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -152,7 +152,7 @@ void Window::closeEvent(QCloseEvent *) void Window::addLayout() { - QVBoxLayout *layout = new QVBoxLayout(this); + auto *layout = new QVBoxLayout(); layout->addWidget(this->notebook_); this->getLayoutContainer()->setLayout(layout); diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index c81abd04d..8a096e27d 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -209,7 +209,7 @@ EmotePopup::EmotePopup(QWidget *parent) this->setStayInScreenRect(true); this->moveTo(this, getApp()->windows->emotePopupPos(), false); - auto *layout = new QVBoxLayout(this); + auto *layout = new QVBoxLayout(); this->getLayoutContainer()->setLayout(layout); QRegularExpression searchRegex("\\S*"); @@ -218,7 +218,7 @@ EmotePopup::EmotePopup(QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - auto *layout2 = new QHBoxLayout(this); + auto *layout2 = new QHBoxLayout(); layout2->setContentsMargins(8, 8, 8, 8); layout2->setSpacing(8); diff --git a/src/widgets/helper/EditableModelView.cpp b/src/widgets/helper/EditableModelView.cpp index 649f85775..97c6840b8 100644 --- a/src/widgets/helper/EditableModelView.cpp +++ b/src/widgets/helper/EditableModelView.cpp @@ -32,7 +32,7 @@ EditableModelView::EditableModelView(QAbstractTableModel *model, bool movable) vbox->setContentsMargins(0, 0, 0, 0); // create button layout - QHBoxLayout *buttons = new QHBoxLayout(this); + auto *buttons = new QHBoxLayout(); this->buttons_ = buttons; vbox->addLayout(buttons); diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 7879e3b92..6f0f55119 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -280,7 +280,7 @@ void SearchPopup::initLayout() // HBOX { - auto *layout2 = new QHBoxLayout(this); + auto *layout2 = new QHBoxLayout(); layout2->setContentsMargins(8, 8, 8, 8); layout2->setSpacing(8); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index a6ae50dae..5b9941ef1 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -1124,7 +1124,7 @@ void Split::showViewerList() viewerDock->move(0, this->header_->height()); auto multiWidget = new QWidget(viewerDock); - auto dockVbox = new QVBoxLayout(viewerDock); + auto *dockVbox = new QVBoxLayout(); auto searchBar = new QLineEdit(viewerDock); auto chattersList = new QListWidget(); From 65a14fb95be97afdba6937048f6522dfd056c77e Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 10 Jun 2023 13:40:30 +0200 Subject: [PATCH 31/83] Fix crash resulting from a mutex deadlock when switching users (#4675) --- CHANGELOG.md | 1 + src/widgets/dialogs/ReplyThreadPopup.cpp | 35 ++++++++++++------------ src/widgets/dialogs/ReplyThreadPopup.hpp | 2 ++ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 285c736ad..fc94beb1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Minor: Improved editing hotkeys. (#4628) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) +- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index fc92da917..1a1c98717 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -138,26 +138,25 @@ void ReplyThreadPopup::addMessagesFromThread() this->setWindowTitle(TEXT_TITLE.arg(this->thread_->root()->loginName, sourceChannel->getName())); - ChannelPtr virtualChannel; if (sourceChannel->isTwitchChannel()) { - virtualChannel = + this->virtualChannel_ = std::make_shared(sourceChannel->getName()); } else { - virtualChannel = std::make_shared(sourceChannel->getName(), - Channel::Type::None); + this->virtualChannel_ = std::make_shared( + sourceChannel->getName(), Channel::Type::None); } - this->ui_.threadView->setChannel(virtualChannel); + this->ui_.threadView->setChannel(this->virtualChannel_); this->ui_.threadView->setSourceChannel(sourceChannel); auto overrideFlags = boost::optional(this->thread_->root()->flags); overrideFlags->set(MessageFlag::DoNotLog); - virtualChannel->addMessage(this->thread_->root(), overrideFlags); + this->virtualChannel_->addMessage(this->thread_->root(), overrideFlags); for (const auto &msgRef : this->thread_->replies()) { if (auto msg = msgRef.lock()) @@ -165,24 +164,24 @@ void ReplyThreadPopup::addMessagesFromThread() auto overrideFlags = boost::optional(msg->flags); overrideFlags->set(MessageFlag::DoNotLog); - virtualChannel->addMessage(msg, overrideFlags); + this->virtualChannel_->addMessage(msg, overrideFlags); } } this->messageConnection_ = std::make_unique( - sourceChannel->messageAppended.connect( - [this, virtualChannel](MessagePtr &message, auto) { - if (message->replyThread == this->thread_) - { - auto overrideFlags = - boost::optional(message->flags); - overrideFlags->set(MessageFlag::DoNotLog); + sourceChannel->messageAppended.connect([this](MessagePtr &message, + auto) { + if (message->replyThread == this->thread_) + { + auto overrideFlags = + boost::optional(message->flags); + overrideFlags->set(MessageFlag::DoNotLog); - // same reply thread, add message - virtualChannel->addMessage(message, overrideFlags); - } - })); + // same reply thread, add message + this->virtualChannel_->addMessage(message, overrideFlags); + } + })); } void ReplyThreadPopup::updateInputUI() diff --git a/src/widgets/dialogs/ReplyThreadPopup.hpp b/src/widgets/dialogs/ReplyThreadPopup.hpp index 863274e5f..613cf4eef 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.hpp +++ b/src/widgets/dialogs/ReplyThreadPopup.hpp @@ -34,6 +34,8 @@ private: std::shared_ptr thread_; // The channel that the reply thread is in ChannelPtr channel_; + // The channel for the `threadView` + ChannelPtr virtualChannel_; Split *split_; struct { From 839ba60fd8f9636e16ea2a60adae0003c57734ac Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 10 Jun 2023 14:38:23 +0200 Subject: [PATCH 32/83] Respect Theme in Input Completion & Quick Switcher (#4671) --- CHANGELOG.md | 1 + .../dialogs/switcher/QuickSwitcherPopup.cpp | 15 ----- src/widgets/listview/GenericListView.cpp | 61 +++++++++++++++---- src/widgets/splits/InputCompletionPopup.cpp | 9 +++ src/widgets/splits/InputCompletionPopup.hpp | 2 + 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc94beb1f..a495f4f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Minor: Improved editing hotkeys. (#4628) +- Minor: The input completion and quick switcher are now styled to match your theme. (#4671) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp index 44cd48fa8..dd33ad54a 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp @@ -143,21 +143,6 @@ void QuickSwitcherPopup::themeChangedEvent() { BasePopup::themeChangedEvent(); - const QString textCol = this->theme->window.text.name(); - const QString bgCol = this->theme->window.background.name(); - - const QString selCol = - (this->theme->isLightTheme() - ? "#68B1FF" // Copied from Theme::splits.input.styleSheet - : this->theme->tabs.selected.backgrounds.regular.name()); - - const QString listStyle = - QString( - "color: %1; background-color: %2; selection-background-color: %3") - .arg(textCol) - .arg(bgCol) - .arg(selCol); - this->ui_.searchEdit->setStyleSheet(this->theme->splits.input.styleSheet); this->ui_.list->refreshTheme(*this->theme); } diff --git a/src/widgets/listview/GenericListView.cpp b/src/widgets/listview/GenericListView.cpp index d1ff57641..978229b13 100644 --- a/src/widgets/listview/GenericListView.cpp +++ b/src/widgets/listview/GenericListView.cpp @@ -103,20 +103,57 @@ bool GenericListView::eventFilter(QObject * /*watched*/, QEvent *event) void GenericListView::refreshTheme(const Theme &theme) { - const QString textCol = theme.window.text.name(); - const QString bgCol = theme.window.background.name(); + const auto textCol = theme.window.text.name(QColor::HexArgb); - const QString selCol = - (theme.isLightTheme() - ? "#68B1FF" // Copied from Theme::splits.input.styleSheet - : theme.tabs.selected.backgrounds.regular.name()); + auto accentColor = theme.accent; + accentColor.setAlpha(100); + const auto selCol = accentColor.name(QColor::HexArgb); - const QString listStyle = - QString( - "color: %1; background-color: %2; selection-background-color: %3") - .arg(textCol) - .arg(bgCol) - .arg(selCol); + const auto listStyle = QStringLiteral(R"( + QListView { + border: none; + color: %1; + background: transparent; + selection-background-color: %2; + } + + QAbstractScrollArea::corner { + border: none; + } + + QScrollBar { + background: transparent; + } + QScrollBar:vertical { + margin-left: 4; + } + QScrollBar:horizontal { + margin-top: 4; + } + + QScrollBar::add-line, + QScrollBar::sub-line, + QScrollBar::left-arrow, + QScrollBar::right-arrow, + QScrollBar::down-arrow, + QScrollBar::up-arrow { + width: 0; + height: 0; + } + + QScrollBar::handle { + background: %2; + border-radius: 4; + } + QScrollBar::handle:vertical { + min-height: 8; + width: 8; + } + QScrollBar::handle:horizontal { + min-width: 8; + height: 8; + })") + .arg(textCol, selCol); this->setStyleSheet(listStyle); } diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index 28eb829ae..81f330b52 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -10,6 +10,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" +#include "singletons/Theme.hpp" #include "util/LayoutCreator.hpp" #include "widgets/listview/GenericListView.hpp" #include "widgets/splits/InputCompletionItem.hpp" @@ -141,6 +142,7 @@ InputCompletionPopup::InputCompletionPopup(QWidget *parent) , model_(this) { this->initLayout(); + this->themeChangedEvent(); QObject::connect(&this->redrawTimer_, &QTimer::timeout, this, [this] { if (this->isVisible()) @@ -227,6 +229,13 @@ void InputCompletionPopup::hideEvent(QHideEvent * /*event*/) this->redrawTimer_.stop(); } +void InputCompletionPopup::themeChangedEvent() +{ + BasePopup::themeChangedEvent(); + + this->ui_.listView->refreshTheme(*getTheme()); +} + void InputCompletionPopup::initLayout() { LayoutCreator creator = {this}; diff --git a/src/widgets/splits/InputCompletionPopup.hpp b/src/widgets/splits/InputCompletionPopup.hpp index 9f36bb5ae..2726f4e0c 100644 --- a/src/widgets/splits/InputCompletionPopup.hpp +++ b/src/widgets/splits/InputCompletionPopup.hpp @@ -50,6 +50,8 @@ protected: void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; + void themeChangedEvent() override; + private: void initLayout(); From c907f2b17067ccf707219c1d5e88070fa6af1d44 Mon Sep 17 00:00:00 2001 From: mohad12211 <51754973+mohad12211@users.noreply.github.com> Date: Sat, 10 Jun 2023 16:44:45 +0300 Subject: [PATCH 33/83] Fix spacing issue with mentions inside RTL text (#4677) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../layouts/MessageLayoutContainer.cpp | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a495f4f51..9b2566718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Minor: Improved editing hotkeys. (#4628) - Minor: The input completion and quick switcher are now styled to match your theme. (#4671) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) +- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 540e9dfac..8f2797880 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -276,17 +276,24 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex) // 2 - in LTR mode, the previous word should be RTL (i.e. reversed) for (int i = startIndex; i <= endIndex; i++) { - if (isNeutral(this->elements_[i]->getText()) && + auto &element = this->elements_[i]; + + const auto neutral = isNeutral(element->getText()); + const auto neutralOrUsername = + neutral || + element->getFlags().hasAny({MessageElementFlag::BoldUsername, + MessageElementFlag::NonBoldUsername}); + + if (neutral && ((this->first == FirstWord::RTL && !this->wasPrevReversed_) || (this->first == FirstWord::LTR && this->wasPrevReversed_))) { - this->elements_[i]->reversedNeutral = true; + element->reversedNeutral = true; } - if (((this->elements_[i]->getText().isRightToLeft() != + if (((element->getText().isRightToLeft() != (this->first == FirstWord::RTL)) && - !isNeutral(this->elements_[i]->getText())) || - (isNeutral(this->elements_[i]->getText()) && - this->wasPrevReversed_)) + !neutralOrUsername) || + (neutralOrUsername && this->wasPrevReversed_)) { swappedSequence.push(i); this->wasPrevReversed_ = true; From 4361790fbdbfdccf9a39e4fc2a94d4ca7999af0a Mon Sep 17 00:00:00 2001 From: Daniel Sage <24928223+dnsge@users.noreply.github.com> Date: Sun, 11 Jun 2023 02:34:28 -0700 Subject: [PATCH 34/83] Add setting to only show tabs with live channels (#4358) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/controllers/hotkeys/ActionNames.hpp | 13 +- src/controllers/hotkeys/HotkeyController.cpp | 4 + src/singletons/Settings.hpp | 4 + src/widgets/Notebook.cpp | 331 ++++++++++++++----- src/widgets/Notebook.hpp | 48 ++- src/widgets/Window.cpp | 81 +++-- src/widgets/dialogs/EmotePopup.cpp | 2 +- src/widgets/helper/NotebookButton.cpp | 4 +- src/widgets/helper/NotebookTab.cpp | 14 +- src/widgets/helper/NotebookTab.hpp | 13 +- src/widgets/settingspages/GeneralPage.cpp | 24 ++ src/widgets/splits/SplitContainer.cpp | 9 +- 13 files changed, 412 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b2566718..0ffa81b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) - Minor: Improved editing hotkeys. (#4628) - Minor: The input completion and quick switcher are now styled to match your theme. (#4671) +- Minor: Added setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) - Bugfix: Fix spacing issue with mentions inside RTL text. (#4677) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index 58a2bb323..d658f05db 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -324,13 +324,18 @@ inline const std::map actionNames{ {"setTabVisibility", ActionDefinition{ .displayName = "Set tab visibility", - .argumentDescription = "[on, off, or toggle. default: toggle]", + .argumentDescription = "[on, off, toggle, liveOnly, or " + "toggleLiveOnly. default: toggle]", .minCountArguments = 0, .maxCountArguments = 1, - .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .possibleArguments{{"Toggle", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, + {"Live only on", {"liveOnly"}}, + {"Live only toggle", {"toggleLiveOnly"}}}, .argumentsPrompt = "New value:", - .argumentsPromptHover = - "Should the tabs be enabled, disabled or toggled.", + .argumentsPromptHover = "Should the tabs be enabled, disabled, " + "toggled, or live-only.", }}, }}, }; diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index df6639756..16a1e0356 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -500,6 +500,10 @@ void HotkeyController::addDefaults(std::set &addedHotkeys) this->tryAddDefault(addedHotkeys, HotkeyCategory::Window, QKeySequence("Ctrl+U"), "setTabVisibility", {"toggle"}, "toggle tab visibility"); + + this->tryAddDefault(addedHotkeys, HotkeyCategory::Window, + QKeySequence("Ctrl+Shift+L"), "setTabVisibility", + {"toggleLiveOnly"}, "toggle live tabs only"); } } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 3d393a6ab..dafa0dc67 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -126,6 +126,10 @@ public: EnumSetting tabDirection = {"/appearance/tabDirection", NotebookTabLocation::Top}; + EnumSetting tabVisibility = { + "/appearance/tabVisibility", + NotebookTabVisibility::AllTabs, + }; // BoolSetting collapseLongMessages = // {"/appearance/messages/collapseLongMessages", false}; diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 40a9237a2..89f1e202e 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -92,9 +92,7 @@ NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) } this->performLayout(); - - tab->show(); - + tab->setVisible(this->shouldShowTab(tab)); return tab; } @@ -216,6 +214,7 @@ void Notebook::select(QWidget *page, bool focusPage) this->selectedPage_ = page; this->performLayout(); + this->updateTabVisibility(); } bool Notebook::containsPage(QWidget *page) @@ -262,45 +261,121 @@ void Notebook::selectIndex(int index, bool focusPage) this->select(this->items_[index].page, focusPage); } -void Notebook::selectNextTab(bool focusPage) +void Notebook::selectVisibleIndex(int index, bool focusPage) { - if (this->items_.size() <= 1) + if (!this->tabVisibilityFilter_) { + this->selectIndex(index, focusPage); return; } - auto index = - (this->indexOf(this->selectedPage_) + 1) % this->items_.count(); + int i = 0; + for (auto &item : this->items_) + { + if (this->tabVisibilityFilter_(item.tab)) + { + if (i == index) + { + // found the index'th visible page + this->select(item.page, focusPage); + return; + } + ++i; + } + } +} - this->select(this->items_[index].page, focusPage); +void Notebook::selectNextTab(bool focusPage) +{ + const int size = this->items_.size(); + + if (!this->tabVisibilityFilter_) + { + if (size <= 1) + { + return; + } + + auto index = (this->indexOf(this->selectedPage_) + 1) % size; + this->select(this->items_[index].page, focusPage); + return; + } + + // find next tab that is permitted by filter + const int startIndex = this->indexOf(this->selectedPage_); + + auto index = (startIndex + 1) % size; + while (index != startIndex) + { + if (this->tabVisibilityFilter_(this->items_[index].tab)) + { + this->select(this->items_[index].page, focusPage); + return; + } + index = (index + 1) % size; + } } void Notebook::selectPreviousTab(bool focusPage) { - if (this->items_.size() <= 1) + const int size = this->items_.size(); + + if (!this->tabVisibilityFilter_) { + if (size <= 1) + { + return; + } + + int index = this->indexOf(this->selectedPage_) - 1; + if (index < 0) + { + index += size; + } + + this->select(this->items_[index].page, focusPage); return; } - int index = this->indexOf(this->selectedPage_) - 1; + // find next previous tab that is permitted by filter + const int startIndex = this->indexOf(this->selectedPage_); - if (index < 0) + auto index = startIndex == 0 ? size - 1 : startIndex - 1; + while (index != startIndex) { - index += this->items_.count(); - } + if (this->tabVisibilityFilter_(this->items_[index].tab)) + { + this->select(this->items_[index].page, focusPage); + return; + } - this->select(this->items_[index].page, focusPage); + index = index == 0 ? size - 1 : index - 1; + } } void Notebook::selectLastTab(bool focusPage) { - const auto size = this->items_.size(); - if (size <= 1) + if (!this->tabVisibilityFilter_) { + const auto size = this->items_.size(); + if (size <= 1) + { + return; + } + + this->select(this->items_[size - 1].page, focusPage); return; } - this->select(this->items_[size - 1].page, focusPage); + // find first tab permitted by filter starting from the end + for (auto it = this->items_.rbegin(); it != this->items_.rend(); ++it) + { + if (this->tabVisibilityFilter_(it->tab)) + { + this->select(it->page, focusPage); + return; + } + } } int Notebook::getPageCount() const @@ -329,6 +404,12 @@ QWidget *Notebook::tabAt(QPoint point, int &index, int maxWidth) for (auto &item : this->items_) { + if (!item.tab->isVisible()) + { + i++; + continue; + } + auto rect = item.tab->getDesiredRect(); rect.setHeight(int(this->scale() * 24)); @@ -381,59 +462,73 @@ void Notebook::setShowTabs(bool value) { this->showTabs_ = value; - this->performLayout(); - for (auto &item : this->items_) - { - item.tab->setHidden(!value); - } - this->setShowAddButton(value); + this->performLayout(); + + this->updateTabVisibility(); + this->updateTabVisibilityMenuAction(); // show a popup upon hiding tabs if (!value && getSettings()->informOnTabVisibilityToggle.getValue()) { - auto unhideSeq = getApp()->hotkeys->getDisplaySequence( - HotkeyCategory::Window, "setTabVisibility", - {std::vector()}); - if (unhideSeq.isEmpty()) - { - unhideSeq = getApp()->hotkeys->getDisplaySequence( - HotkeyCategory::Window, "setTabVisibility", {{"toggle"}}); - } - if (unhideSeq.isEmpty()) - { - unhideSeq = getApp()->hotkeys->getDisplaySequence( - HotkeyCategory::Window, "setTabVisibility", {{"on"}}); - } - QString hotkeyInfo = "(currently unbound)"; - if (!unhideSeq.isEmpty()) - { - hotkeyInfo = - "(" + - unhideSeq.toString(QKeySequence::SequenceFormat::NativeText) + - ")"; - } - QMessageBox msgBox(this->window()); - msgBox.window()->setWindowTitle("Chatterino - hidden tabs"); - msgBox.setText("You've just hidden your tabs."); - msgBox.setInformativeText( - "You can toggle tabs by using the keyboard shortcut " + hotkeyInfo + - " or right-clicking the tab area and selecting \"Toggle " - "visibility of tabs\"."); - msgBox.addButton(QMessageBox::Ok); - auto *dsaButton = - msgBox.addButton("Don't show again", QMessageBox::YesRole); - - msgBox.setDefaultButton(QMessageBox::Ok); - - msgBox.exec(); - - if (msgBox.clickedButton() == dsaButton) - { - getSettings()->informOnTabVisibilityToggle.setValue(false); - } + this->showTabVisibilityInfoPopup(); + } +} + +void Notebook::showTabVisibilityInfoPopup() +{ + auto unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {std::vector()}); + if (unhideSeq.isEmpty()) + { + unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"toggle"}}); + } + if (unhideSeq.isEmpty()) + { + unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"on"}}); + } + QString hotkeyInfo = "(currently unbound)"; + if (!unhideSeq.isEmpty()) + { + hotkeyInfo = + "(" + unhideSeq.toString(QKeySequence::SequenceFormat::NativeText) + + ")"; + } + QMessageBox msgBox(this->window()); + msgBox.window()->setWindowTitle("Chatterino - hidden tabs"); + msgBox.setText("You've just hidden your tabs."); + msgBox.setInformativeText( + "You can toggle tabs by using the keyboard shortcut " + hotkeyInfo + + " or right-clicking the tab area and selecting \"Toggle " + "visibility of tabs\"."); + msgBox.addButton(QMessageBox::Ok); + auto *dsaButton = + msgBox.addButton("Don't show again", QMessageBox::YesRole); + + msgBox.setDefaultButton(QMessageBox::Ok); + + msgBox.exec(); + + if (msgBox.clickedButton() == dsaButton) + { + getSettings()->informOnTabVisibilityToggle.setValue(false); + } +} + +void Notebook::refresh() +{ + this->performLayout(); + this->updateTabVisibility(); +} + +void Notebook::updateTabVisibility() +{ + for (auto &item : this->items_) + { + item.tab->setVisible(this->shouldShowTab(item.tab)); } - updateTabVisibilityMenuAction(); } void Notebook::updateTabVisibilityMenuAction() @@ -510,6 +605,21 @@ void Notebook::performLayout(bool animated) const auto buttonWidth = tabHeight; const auto buttonHeight = tabHeight - 1; + std::vector filteredItems; + filteredItems.reserve(this->items_.size()); + if (this->tabVisibilityFilter_) + { + std::copy_if(this->items_.begin(), this->items_.end(), + std::back_inserter(filteredItems), + [this](const auto &item) { + return this->tabVisibilityFilter_(item.tab); + }); + } + else + { + filteredItems.assign(this->items_.begin(), this->items_.end()); + } + if (this->tabLocation_ == NotebookTabLocation::Top) { auto x = left; @@ -535,14 +645,14 @@ void Notebook::performLayout(bool animated) { // layout tabs /// Notebook tabs need to know if they are in the last row. - auto firstInBottomRow = - this->items_.size() ? &this->items_.front() : nullptr; + auto *firstInBottomRow = + filteredItems.empty() ? nullptr : &filteredItems.front(); - for (auto &item : this->items_) + for (auto &item : filteredItems) { /// Break line if element doesn't fit. - auto isFirst = &item == &this->items_.front(); - auto isLast = &item == &this->items_.back(); + auto isFirst = &item == &filteredItems.front(); + auto isLast = &item == &filteredItems.back(); auto fitsInLine = ((isLast ? addButtonWidth : 0) + x + item.tab->width()) <= width(); @@ -562,7 +672,7 @@ void Notebook::performLayout(bool animated) /// Update which tabs are in the last row auto inLastRow = false; - for (const auto &item : this->items_) + for (const auto &item : filteredItems) { if (&item == firstInBottomRow) { @@ -633,7 +743,7 @@ void Notebook::performLayout(bool animated) { return; } - int count = this->items_.size() + (this->showAddButton_ ? 1 : 0); + int count = filteredItems.size() + (this->showAddButton_ ? 1 : 0); int columnCount = ceil((float)count / tabsPerColumn); // only add width of all the tabs if they are not hidden @@ -644,13 +754,15 @@ void Notebook::performLayout(bool animated) bool isLastColumn = col == columnCount - 1; auto largestWidth = 0; int tabStart = col * tabsPerColumn; - int tabEnd = std::min((col + 1) * tabsPerColumn, - (int)this->items_.size()); + int tabEnd = + std::min(static_cast((col + 1) * tabsPerColumn), + filteredItems.size()); for (int i = tabStart; i < tabEnd; i++) { - largestWidth = std::max( - this->items_.at(i).tab->normalTabWidth(), largestWidth); + largestWidth = + std::max(filteredItems.at(i).tab->normalTabWidth(), + largestWidth); } if (isLastColumn && this->showAddButton_) @@ -664,7 +776,7 @@ void Notebook::performLayout(bool animated) for (int i = tabStart; i < tabEnd; i++) { - auto item = this->items_.at(i); + auto item = filteredItems.at(i); /// Layout tab item.tab->growWidth(largestWidth); @@ -735,7 +847,7 @@ void Notebook::performLayout(bool animated) { return; } - int count = this->items_.size() + (this->showAddButton_ ? 1 : 0); + int count = filteredItems.size() + (this->showAddButton_ ? 1 : 0); int columnCount = ceil((float)count / tabsPerColumn); // only add width of all the tabs if they are not hidden @@ -746,13 +858,15 @@ void Notebook::performLayout(bool animated) bool isLastColumn = col == columnCount - 1; auto largestWidth = 0; int tabStart = col * tabsPerColumn; - int tabEnd = std::min((col + 1) * tabsPerColumn, - (int)this->items_.size()); + int tabEnd = + std::min(static_cast((col + 1) * tabsPerColumn), + filteredItems.size()); for (int i = tabStart; i < tabEnd; i++) { - largestWidth = std::max( - this->items_.at(i).tab->normalTabWidth(), largestWidth); + largestWidth = + std::max(filteredItems.at(i).tab->normalTabWidth(), + largestWidth); } if (isLastColumn && this->showAddButton_) @@ -771,7 +885,7 @@ void Notebook::performLayout(bool animated) for (int i = tabStart; i < tabEnd; i++) { - auto item = this->items_.at(i); + auto item = filteredItems.at(i); /// Layout tab item.tab->growWidth(largestWidth); @@ -840,14 +954,14 @@ void Notebook::performLayout(bool animated) // layout tabs /// Notebook tabs need to know if they are in the last row. - auto firstInBottomRow = - this->items_.size() ? &this->items_.front() : nullptr; + auto *firstInBottomRow = + filteredItems.empty() ? nullptr : &filteredItems.front(); - for (auto &item : this->items_) + for (auto &item : filteredItems) { /// Break line if element doesn't fit. - auto isFirst = &item == &this->items_.front(); - auto isLast = &item == &this->items_.back(); + auto isFirst = &item == &filteredItems.front(); + auto isLast = &item == &filteredItems.back(); auto fitsInLine = ((isLast ? addButtonWidth : 0) + x + item.tab->width()) <= width(); @@ -867,7 +981,7 @@ void Notebook::performLayout(bool animated) /// Update which tabs are in the last row auto inLastRow = false; - for (const auto &item : this->items_) + for (const auto &item : filteredItems) { if (&item == firstInBottomRow) { @@ -1046,6 +1160,28 @@ size_t Notebook::visibleButtonCount() const return i; } +void Notebook::setTabVisibilityFilter(TabVisibilityFilter filter) +{ + this->tabVisibilityFilter_ = std::move(filter); + this->performLayout(); + this->updateTabVisibility(); +} + +bool Notebook::shouldShowTab(const NotebookTab *tab) const +{ + if (!this->showTabs_) + { + return false; + } + + if (this->tabVisibilityFilter_) + { + return this->tabVisibilityFilter_(tab); + } + + return true; +} + SplitNotebook::SplitNotebook(Window *parent) : Notebook(parent) { @@ -1061,6 +1197,24 @@ SplitNotebook::SplitNotebook(Window *parent) this->addCustomButtons(); } + getSettings()->tabVisibility.connect( + [this](int val, auto) { + auto visibility = NotebookTabVisibility(val); + switch (visibility) + { + case NotebookTabVisibility::LiveOnly: + this->setTabVisibilityFilter([](const NotebookTab *tab) { + return tab->isLive() || tab->isSelected(); + }); + break; + case NotebookTabVisibility::AllTabs: + default: + this->setTabVisibilityFilter(nullptr); + break; + } + }, + this->signalHolder_, true); + this->signalHolder_.managedConnect( getApp()->windows->selectSplit, [this](Split *split) { for (auto &&item : this->items()) @@ -1204,7 +1358,6 @@ SplitContainer *SplitNotebook::addPage(bool select) auto tab = Notebook::addPage(container, QString(), select); container->setTab(tab); tab->setParent(this); - tab->setVisible(this->getShowTabs()); return container; } diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 382b260ec..eb41af63a 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -9,6 +9,8 @@ #include #include +#include + namespace chatterino { class Window; @@ -19,6 +21,17 @@ class SplitContainer; enum NotebookTabLocation { Top = 0, Left = 1, Right = 2, Bottom = 3 }; +// Controls the visibility of tabs in this notebook +enum NotebookTabVisibility : int { + // Show all tabs + AllTabs = 0, + + // Only show tabs containing splits that are live + LiveOnly = 1, +}; + +using TabVisibilityFilter = std::function; + class Notebook : public BaseWidget { Q_OBJECT @@ -35,6 +48,7 @@ public: int indexOf(QWidget *page) const; virtual void select(QWidget *page, bool focusPage = true); void selectIndex(int index, bool focusPage = true); + void selectVisibleIndex(int index, bool focusPage = true); void selectNextTab(bool focusPage = true); void selectPreviousTab(bool focusPage = true); void selectLastTab(bool focusPage = true); @@ -56,8 +70,6 @@ public: bool getShowAddButton() const; void setShowAddButton(bool value); - void performLayout(bool animate = false); - void setTabLocation(NotebookTabLocation location); bool isNotebookLayoutLocked() const; @@ -65,6 +77,9 @@ public: void addNotebookActionsToMenu(QMenu *menu); + // Update layout and tab visibility + void refresh(); + protected: virtual void scaleChangedEvent(float scale_) override; virtual void resizeEvent(QResizeEvent *) override; @@ -85,7 +100,32 @@ protected: return items_; } + /** + * @brief Apply the given tab visibility filter + * + * An empty function can be provided to denote that no filter will be applied + * + * Tabs will be redrawn after this function is called. + **/ + void setTabVisibilityFilter(TabVisibilityFilter filter); + + /** + * @brief shouldShowTab has the final say whether a tab should be visible right now. + **/ + bool shouldShowTab(const NotebookTab *tab) const; + private: + void performLayout(bool animate = false); + + /** + * @brief Show a popup informing the user of some big tab visibility changes + **/ + void showTabVisibilityInfoPopup(); + + /** + * @brief Updates the visibility state of all tabs + **/ + void updateTabVisibility(); void updateTabVisibilityMenuAction(); void resizeAddButton(); @@ -113,6 +153,10 @@ private: NotebookTabLocation tabLocation_ = NotebookTabLocation::Top; QAction *lockNotebookLayoutAction_; QAction *showTabsAction_; + + // This filter, if set, is used to figure out the visibility of + // the tabs in this notebook. + TabVisibilityFilter tabVisibilityFilter_; }; class SplitNotebook : public Notebook diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index efcd33d4e..d36872811 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -354,7 +354,7 @@ void Window::addShortcuts() int result = target.toInt(&ok); if (ok) { - this->notebook_->selectIndex(result); + this->notebook_->selectVisibleIndex(result); } else { @@ -619,46 +619,61 @@ void Window::addShortcuts() }}, {"setTabVisibility", [this](std::vector arguments) -> QString { - auto mode = 2; - if (arguments.size() != 0) + QString arg = arguments.empty() ? "toggle" : arguments.front(); + + if (arg == "off") { - auto arg = arguments.at(0); - if (arg == "off") + this->notebook_->setShowTabs(false); + getSettings()->tabVisibility.setValue( + NotebookTabVisibility::AllTabs); + } + else if (arg == "on") + { + this->notebook_->setShowTabs(true); + getSettings()->tabVisibility.setValue( + NotebookTabVisibility::AllTabs); + } + else if (arg == "toggle") + { + this->notebook_->setShowTabs(!this->notebook_->getShowTabs()); + getSettings()->tabVisibility.setValue( + NotebookTabVisibility::AllTabs); + } + else if (arg == "liveOnly") + { + this->notebook_->setShowTabs(true); + getSettings()->tabVisibility.setValue( + NotebookTabVisibility::LiveOnly); + } + else if (arg == "toggleLiveOnly") + { + if (!this->notebook_->getShowTabs()) { - mode = 0; - } - else if (arg == "on") - { - mode = 1; - } - else if (arg == "toggle") - { - mode = 2; + // Tabs are currently hidden, so the intention is to show + // tabs again before enabling the live only setting + this->notebook_->setShowTabs(true); + getSettings()->tabVisibility.setValue( + NotebookTabVisibility::LiveOnly); } else { - qCWarning(chatterinoHotkeys) - << "Invalid argument for setStreamerMode hotkey: " - << arg; - return QString("Invalid argument for setTabVisibility " - "hotkey: %1. Use \"on\", \"off\" or " - "\"toggle\".") - .arg(arg); + getSettings()->tabVisibility.setValue( + getSettings()->tabVisibility.getEnum() == + NotebookTabVisibility::LiveOnly + ? NotebookTabVisibility::AllTabs + : NotebookTabVisibility::LiveOnly); } } + else + { + qCWarning(chatterinoHotkeys) + << "Invalid argument for setTabVisibility hotkey: " << arg; + return QString("Invalid argument for setTabVisibility hotkey: " + "%1. Use \"on\", \"off\", \"toggle\", " + "\"liveOnly\", or \"toggleLiveOnly\".") + .arg(arg); + } - if (mode == 0) - { - this->notebook_->setShowTabs(false); - } - else if (mode == 1) - { - this->notebook_->setShowTabs(true); - } - else if (mode == 2) - { - this->notebook_->setShowTabs(!this->notebook_->getShowTabs()); - } return ""; }}, }; diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 8a096e27d..2c4d91256 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -310,7 +310,7 @@ void EmotePopup::addShortcuts() int result = target.toInt(&ok); if (ok) { - this->notebook_->selectIndex(result, false); + this->notebook_->selectVisibleIndex(result, false); } else { diff --git a/src/widgets/helper/NotebookButton.cpp b/src/widgets/helper/NotebookButton.cpp index f6cc67b4d..b7e8315ac 100644 --- a/src/widgets/helper/NotebookButton.cpp +++ b/src/widgets/helper/NotebookButton.cpp @@ -211,12 +211,12 @@ void NotebookButton::dropEvent(QDropEvent *event) void NotebookButton::hideEvent(QHideEvent *) { - this->parent_->performLayout(); + this->parent_->refresh(); } void NotebookButton::showEvent(QShowEvent *) { - this->parent_->performLayout(); + this->parent_->refresh(); } } // namespace chatterino diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index 539d44263..297ccf19b 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -234,7 +234,7 @@ void NotebookTab::updateSize() if (this->width() != width || this->height() != height) { this->resize(width, height); - this->notebook_->performLayout(); + this->notebook_->refresh(); } } @@ -290,7 +290,7 @@ void NotebookTab::titleUpdated() { // Queue up save because: Tab title changed getApp()->windows->queueSave(); - this->notebook_->performLayout(); + this->notebook_->refresh(); this->updateSize(); this->update(); } @@ -327,13 +327,21 @@ void NotebookTab::setTabLocation(NotebookTabLocation location) } } -void NotebookTab::setLive(bool isLive) +bool NotebookTab::setLive(bool isLive) { if (this->isLive_ != isLive) { this->isLive_ = isLive; this->update(); + return true; } + + return false; +} + +bool NotebookTab::isLive() const +{ + return this->isLive_; } void NotebookTab::setHighlightState(HighlightState newHighlightStyle) diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 36d622624..dfc5b370f 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -40,7 +40,18 @@ public: void setInLastRow(bool value); void setTabLocation(NotebookTabLocation location); - void setLive(bool isLive); + /** + * @brief Sets the live status of this tab + * + * Returns true if the live status was changed, false if nothing changed. + **/ + bool setLive(bool isLive); + + /** + * @brief Returns true if any split in this tab is live + **/ + bool isLive() const; + void setHighlightState(HighlightState style); void setHighlightsEnabled(const bool &newVal); bool hasHighlightsEnabled() const; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 5bf6fe675..5c9ab87d8 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -199,6 +199,30 @@ void GeneralPage::initLayout(GeneralPageView &layout) tabDirectionDropdown->setMinimumWidth( tabDirectionDropdown->minimumSizeHint().width()); + layout.addDropdown::type>( + "Tab visibility", {"All tabs", "Only live tabs"}, s.tabVisibility, + [](auto val) { + switch (val) + { + case NotebookTabVisibility::LiveOnly: + return "Only live tabs"; + case NotebookTabVisibility::AllTabs: + default: + return "All tabs"; + } + }, + [](auto args) { + if (args.value == "Only live tabs") + { + return NotebookTabVisibility::LiveOnly; + } + else + { + return NotebookTabVisibility::AllTabs; + } + }, + false, "Choose which tabs are visible in the notebook"); + layout.addCheckbox( "Show message reply context", s.hideReplyContext, true, "This setting will only affect how messages are shown. You can reply " diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index d7de47402..43463f083 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -935,7 +935,14 @@ void SplitContainer::refreshTabLiveStatus() } } - this->tab_->setLive(liveStatus); + if (this->tab_->setLive(liveStatus)) + { + auto *notebook = dynamic_cast(this->parentWidget()); + if (notebook) + { + notebook->refresh(); + } + } } // From a045d3ee81338ff81abc5c11936e46703e3006bf Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 11 Jun 2023 12:31:04 +0200 Subject: [PATCH 35/83] Use `sccache` on Windows (#4678) * build: support sccache and windows --- .github/workflows/build.yml | 10 ++++++++++ .gitignore | 3 +++ CHANGELOG.md | 2 ++ CMakeLists.txt | 25 ++++++++++++++++++++++--- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ac21fb45..4094a5a82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -134,6 +134,16 @@ jobs: "C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV" shell: powershell + - name: Setup sccache (Windows) + # sccache v0.5.3 + uses: nerixyz/ccache-action@9a7e8d00116ede600ee7717350c6594b8af6aaa5 + if: startsWith(matrix.os, 'windows') + with: + variant: sccache + key: sccache-build-${{ matrix.os }}-${{ matrix.qt-version }}-${{ matrix.skip-crashpad }} + restore-keys: | + sccache-build-${{ matrix.os }}-${{ matrix.qt-version }} + - name: Cache conan packages (Windows) if: startsWith(matrix.os, 'windows') uses: actions/cache@v3 diff --git a/.gitignore b/.gitignore index 9e5859235..620f233a9 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,6 @@ resources/resources_autogenerated.qrc # Leftovers from running `aqt install` aqtinstall.log + +# sccache (CI) +.sccache diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffa81b22..f8d8cf426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ - Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666) - Dev: Fixed warnings about QWidgets already having a QLayout. (#4672) - Dev: Fixed undefined behavior when loading non-existant credentials. (#4673) +- Dev: Added support for compiling with `sccache`. (#4678) +- Dev: Added `sccache` in Windows CI. (#4678) ## 2.4.4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0714104ee..d433a0ec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,11 +42,30 @@ else() endif() find_program(CCACHE_PROGRAM ccache) -if (CCACHE_PROGRAM) - set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") - message("Using ${CCACHE_PROGRAM} for speeding up build") +find_program(SCCACHE_PROGRAM sccache) +if (SCCACHE_PROGRAM) + set(_compiler_launcher ${SCCACHE_PROGRAM}) +elseif (CCACHE_PROGRAM) + set(_compiler_launcher ${CCACHE_PROGRAM}) endif () +if (_compiler_launcher) + set(CMAKE_CXX_COMPILER_LAUNCHER "${_compiler_launcher}" CACHE STRING "CXX compiler launcher") + message(STATUS "Using ${_compiler_launcher} for speeding up build") + + if (MSVC) + # /Zi can't be used with (s)ccache + # Use /Z7 instead (debug info in object files) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") + elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + endif() + endif() +endif() + include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake) find_package(Qt${MAJOR_QT_VERSION} REQUIRED From 36bc8e0520146ac4108f51569822471389b2f876 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jun 2023 20:11:01 +0200 Subject: [PATCH 36/83] Bump lib/crashpad from `ec99257` to `432ff49` (#4688) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rasmus Karlsson Co-authored-by: nerix --- lib/crashpad | 2 +- src/providers/Crashpad.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/crashpad b/lib/crashpad index ec9925786..432ff49ec 160000 --- a/lib/crashpad +++ b/lib/crashpad @@ -1 +1 @@ -Subproject commit ec992578688b4c51c1856d08731cf7dcf10e446a +Subproject commit 432ff49ecccc1cdebf1a7646007bb0594ac3481f diff --git a/src/providers/Crashpad.cpp b/src/providers/Crashpad.cpp index 4c2fb9760..f81cbe071 100644 --- a/src/providers/Crashpad.cpp +++ b/src/providers/Crashpad.cpp @@ -79,8 +79,8 @@ std::unique_ptr installCrashHandler() // See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md // for documentation on available options. - if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, true, - false)) + if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, {}, + true, false)) { qCDebug(chatterinoApp) << "Failed to start crashpad handler"; return nullptr; From 2d3d3ae46e78d632170c68047c25217c0b9c9e82 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 17 Jun 2023 17:01:16 +0200 Subject: [PATCH 37/83] Make sanitizers truly optional (#4689) --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d433a0ec9..340e8a64d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ if (WIN32) find_package(WinToast REQUIRED) endif () -find_package(Sanitizers) +find_package(Sanitizers QUIET) # Find boost on the system # `OPTIONAL_COMPONENTS random` is required for vcpkg builds to link. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 465a147f1..b7465ebdb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -710,7 +710,12 @@ if (BUILD_APP) else() add_executable(${EXECUTABLE_PROJECT} main.cpp) endif() - add_sanitizers(${EXECUTABLE_PROJECT}) + + if(COMMAND add_sanitizers) + add_sanitizers(${EXECUTABLE_PROJECT}) + else() + message(WARNING "Sanitizers support is disabled") + endif() target_include_directories(${EXECUTABLE_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/) From aff93426476e6727f269ac7c2eb26d4540810a2b Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 17 Jun 2023 17:41:52 +0200 Subject: [PATCH 38/83] Add option to subscribe to and pin reply threads (#4680) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../highlights/HighlightController.cpp | 2 +- src/controllers/highlights/HighlightModel.cpp | 2 +- src/messages/Message.hpp | 2 +- src/messages/MessageThread.cpp | 22 ++++-- src/messages/MessageThread.hpp | 29 +++++++- src/providers/twitch/IrcMessageHandler.cpp | 43 ++++++----- src/singletons/Settings.hpp | 4 + src/widgets/DraggablePopup.cpp | 31 +++++++- src/widgets/DraggablePopup.hpp | 5 ++ src/widgets/dialogs/ReplyThreadPopup.cpp | 74 +++++++++++++++++-- src/widgets/dialogs/ReplyThreadPopup.hpp | 5 ++ src/widgets/dialogs/UserInfoPopup.cpp | 33 +-------- src/widgets/dialogs/UserInfoPopup.hpp | 3 - src/widgets/settingspages/GeneralPage.cpp | 7 ++ 15 files changed, 194 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d8cf426..fbe09aee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Minor: Improved editing hotkeys. (#4628) - Minor: The input completion and quick switcher are now styled to match your theme. (#4671) - Minor: Added setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358) +- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) - Bugfix: Fix spacing issue with mentions inside RTL text. (#4677) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index bd517863f..ae85fdea3 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -163,7 +163,7 @@ void rebuildReplyThreadHighlight(Settings &settings, const auto & /*senderName*/, const auto & /*originalMessage*/, const auto &flags, const auto self) -> boost::optional { - if (flags.has(MessageFlag::ParticipatedThread) && !self) + if (flags.has(MessageFlag::SubscribedThread) && !self) { return HighlightResult{ highlightAlert, diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index 13ff5ec6b..b49f6fbb4 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -210,7 +210,7 @@ void HighlightModel::afterInit() std::vector threadMessageRow = this->createRow(); setBoolItem(threadMessageRow[Column::Pattern], getSettings()->enableThreadHighlight.getValue(), true, false); - threadMessageRow[Column::Pattern]->setData("Participated Reply Threads", + threadMessageRow[Column::Pattern]->setData("Subscribed Reply Threads", Qt::DisplayRole); setBoolItem(threadMessageRow[Column::ShowInMentions], getSettings()->showThreadHighlightInMentions.getValue(), true, diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index bea40a1b1..83e311b1c 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -46,7 +46,7 @@ enum class MessageFlag : int64_t { FirstMessage = (1LL << 23), ReplyMessage = (1LL << 24), ElevatedMessage = (1LL << 25), - ParticipatedThread = (1LL << 26), + SubscribedThread = (1LL << 26), CheerMessage = (1LL << 27), LiveUpdatesAdd = (1LL << 28), LiveUpdatesRemove = (1LL << 29), diff --git a/src/messages/MessageThread.cpp b/src/messages/MessageThread.cpp index 0ffdc3f18..e1227ab09 100644 --- a/src/messages/MessageThread.cpp +++ b/src/messages/MessageThread.cpp @@ -1,4 +1,4 @@ -#include "MessageThread.hpp" +#include "messages/MessageThread.hpp" #include "messages/Message.hpp" #include "util/DebugCount.hpp" @@ -58,14 +58,26 @@ size_t MessageThread::liveCount( return count; } -bool MessageThread::participated() const +void MessageThread::markSubscribed() { - return this->participated_; + if (this->subscription_ == Subscription::Subscribed) + { + return; + } + + this->subscription_ = Subscription::Subscribed; + this->subscriptionUpdated(); } -void MessageThread::markParticipated() +void MessageThread::markUnsubscribed() { - this->participated_ = true; + if (this->subscription_ == Subscription::Unsubscribed) + { + return; + } + + this->subscription_ = Subscription::Unsubscribed; + this->subscriptionUpdated(); } } // namespace chatterino diff --git a/src/messages/MessageThread.hpp b/src/messages/MessageThread.hpp index ae0d24794..442db46a6 100644 --- a/src/messages/MessageThread.hpp +++ b/src/messages/MessageThread.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -11,6 +12,12 @@ struct Message; class MessageThread { public: + enum class Subscription : uint8_t { + None, + Subscribed, + Unsubscribed, + }; + MessageThread(std::shared_ptr rootMessage); ~MessageThread(); @@ -23,9 +30,22 @@ public: /// Returns the number of live reply references size_t liveCount(const std::shared_ptr &exclude) const; - bool participated() const; + bool subscribed() const + { + return this->subscription_ == Subscription::Subscribed; + } - void markParticipated(); + /// Returns true if and only if the user manually unsubscribed from the thread + /// @see #markUnsubscribed() + bool unsubscribed() const + { + return this->subscription_ == Subscription::Unsubscribed; + } + + /// Subscribe to this thread. + void markSubscribed(); + /// Unsubscribe from this thread. + void markUnsubscribed(); const QString &rootId() const { @@ -42,11 +62,14 @@ public: return replies_; } + boost::signals2::signal subscriptionUpdated; + private: const QString rootMessageId_; const std::shared_ptr rootMessage_; std::vector> replies_; - bool participated_ = false; + + Subscription subscription_ = Subscription::None; }; } // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index ce2752bf9..280d8a95f 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -119,31 +119,40 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, { const auto ¤tLogin = getApp()->accounts->twitch.getCurrent()->getUserName(); - if (thread->participated()) + + if (thread->subscribed()) { - builder.message().flags.set(MessageFlag::ParticipatedThread); + builder.message().flags.set(MessageFlag::SubscribedThread); return; } - if (isNew) + if (thread->unsubscribed()) { - if (const auto it = tags.find("reply-parent-user-login"); - it != tags.end()) - { - auto name = it.value().toString(); - if (name == currentLogin) - { - thread->markParticipated(); - builder.message().flags.set(MessageFlag::ParticipatedThread); - return; // already marked as participated - } - } + return; } - if (senderLogin == currentLogin) + if (getSettings()->autoSubToParticipatedThreads) { - thread->markParticipated(); - // don't set the highlight here + if (isNew) + { + if (const auto it = tags.find("reply-parent-user-login"); + it != tags.end()) + { + auto name = it.value().toString(); + if (name == currentLogin) + { + thread->markSubscribed(); + builder.message().flags.set(MessageFlag::SubscribedThread); + return; // already marked as participated + } + } + } + + if (senderLogin == currentLogin) + { + thread->markSubscribed(); + // don't set the highlight here + } } } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index dafa0dc67..9440f8a97 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -194,6 +194,10 @@ public: BoolSetting autoCloseUserPopup = {"/behaviour/autoCloseUserPopup", true}; BoolSetting autoCloseThreadPopup = {"/behaviour/autoCloseThreadPopup", false}; + BoolSetting autoSubToParticipatedThreads = { + "/behaviour/autoSubToParticipatedThreads", + true, + }; // BoolSetting twitchSeperateWriteConnection = // {"/behaviour/twitchSeperateWriteConnection", false}; diff --git a/src/widgets/DraggablePopup.cpp b/src/widgets/DraggablePopup.cpp index ca015282b..157150a9a 100644 --- a/src/widgets/DraggablePopup.cpp +++ b/src/widgets/DraggablePopup.cpp @@ -1,4 +1,8 @@ -#include "DraggablePopup.hpp" +#include "widgets/DraggablePopup.hpp" + +#include "singletons/Resources.hpp" +#include "singletons/Theme.hpp" +#include "widgets/helper/Button.hpp" #include @@ -90,4 +94,29 @@ void DraggablePopup::mouseMoveEvent(QMouseEvent *event) } } +Button *DraggablePopup::createPinButton() +{ + auto *button = new Button(this); + button->setPixmap(getTheme()->buttons.pin); + button->setScaleIndependantSize(18, 18); + button->setToolTip("Pin Window"); + + bool pinned = false; + QObject::connect( + button, &Button::leftClicked, [this, button, pinned]() mutable { + pinned = !pinned; + if (pinned) + { + this->setActionOnFocusLoss(BaseWindow::Nothing); + button->setPixmap(getResources().buttons.pinEnabled); + } + else + { + this->setActionOnFocusLoss(BaseWindow::Delete); + button->setPixmap(getTheme()->buttons.pin); + } + }); + return button; +} + } // namespace chatterino diff --git a/src/widgets/DraggablePopup.hpp b/src/widgets/DraggablePopup.hpp index 65050e15b..578ec8dfa 100644 --- a/src/widgets/DraggablePopup.hpp +++ b/src/widgets/DraggablePopup.hpp @@ -26,6 +26,11 @@ protected: void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; + /// Creates a pin button that is scoped to this window. + /// When clicked, the user can toggle whether the window is pinned. + /// The window is considered unpinned at the start. + Button *createPinButton(); + // lifetimeHack_ is used to check that the window hasn't been destroyed yet std::shared_ptr lifetimeHack_; diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index 1a1c98717..16c37cdd7 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -7,16 +7,18 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "messages/Message.hpp" #include "messages/MessageThread.hpp" -#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Settings.hpp" #include "util/LayoutCreator.hpp" +#include "widgets/helper/Button.hpp" #include "widgets/helper/ChannelView.hpp" -#include "widgets/helper/ResizingTextEdit.hpp" #include "widgets/Scrollbar.hpp" #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitInput.hpp" +#include + const QString TEXT_TITLE("Reply Thread - @%1 in #%2"); namespace chatterino { @@ -72,9 +74,6 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory( HotkeyCategory::PopupWindow, actions, this); - auto layout = LayoutCreator(this->getLayoutContainer()) - .setLayoutType(); - // initialize UI this->ui_.threadView = new ChannelView(this, this->split_, ChannelView::Context::ReplyThread); @@ -110,10 +109,57 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, } }); + auto layout = LayoutCreator(this->getLayoutContainer()) + .setLayoutType(); + layout->setSpacing(0); // provide draggable margin if frameless auto marginPx = closeAutomatically ? 15 : 1; layout->setContentsMargins(marginPx, marginPx, marginPx, marginPx); + + // Top Row + bool addCheckbox = getSettings()->enableThreadHighlight; + if (addCheckbox || closeAutomatically) + { + auto *hbox = new QHBoxLayout(); + + if (addCheckbox) + { + this->ui_.notificationCheckbox = + new QCheckBox("Subscribe to thread", this); + QObject::connect(this->ui_.notificationCheckbox, + &QCheckBox::toggled, [this](bool checked) { + if (!this->thread_ || + this->thread_->subscribed() == checked) + { + return; + } + + if (checked) + { + this->thread_->markSubscribed(); + } + else + { + this->thread_->markUnsubscribed(); + } + }); + hbox->addWidget(this->ui_.notificationCheckbox, 1); + } + + if (closeAutomatically) + { + hbox->addWidget(this->createPinButton(), 0, Qt::AlignRight); + hbox->setContentsMargins(0, 0, 0, 5); + } + else + { + hbox->setContentsMargins(10, 0, 0, 4); + } + + layout->addLayout(hbox, 1); + } + layout->addWidget(this->ui_.threadView, 1); layout->addWidget(this->ui_.replyInput); } @@ -124,6 +170,24 @@ void ReplyThreadPopup::setThread(std::shared_ptr thread) this->ui_.replyInput->setReply(this->thread_); this->addMessagesFromThread(); this->updateInputUI(); + + if (!this->thread_) [[unlikely]] + { + this->replySubscriptionSignal_ = boost::signals2::scoped_connection{}; + return; + } + + auto updateCheckbox = [this]() { + if (this->ui_.notificationCheckbox) + { + this->ui_.notificationCheckbox->setChecked( + this->thread_->subscribed()); + } + }; + updateCheckbox(); + + this->replySubscriptionSignal_ = + this->thread_->subscriptionUpdated.connect(updateCheckbox); } void ReplyThreadPopup::addMessagesFromThread() diff --git a/src/widgets/dialogs/ReplyThreadPopup.hpp b/src/widgets/dialogs/ReplyThreadPopup.hpp index 613cf4eef..567812ce0 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.hpp +++ b/src/widgets/dialogs/ReplyThreadPopup.hpp @@ -7,6 +7,8 @@ #include #include +class QCheckBox; + namespace chatterino { class MessageThread; @@ -41,10 +43,13 @@ private: struct { ChannelView *threadView = nullptr; SplitInput *replyInput = nullptr; + + QCheckBox *notificationCheckbox = nullptr; } ui_; std::unique_ptr messageConnection_; std::vector bSignals_; + boost::signals2::scoped_connection replySubscriptionSignal_; }; } // namespace chatterino diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 9c0541d63..e69781002 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -142,7 +142,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, "split being nullptr causes lots of bugs down the road"); this->setWindowTitle("Usercard"); this->setStayInScreenRect(true); - this->updateFocusLoss(); HotkeyController::HotkeyMap actions{ {"delete", @@ -361,17 +360,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, // button to pin the window (only if we close automatically) if (this->closeAutomatically_) { - this->ui_.pinButton = box.emplace