diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd3acb1437..30ee83b3dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,16 @@ - Minor: Added autocompletion in /whispers for Twitch emotes, Global Bttv/Ffz emotes and emojis. (#2999, #3033) - Minor: Received Twitch messages now use the exact same timestamp (obtained from Twitch's server) for every Chatterino user instead of assuming message timestamp on client's side. (#3021) - Minor: Received IRC messages use `time` message tag for timestamp if it's available. (#3021) +- Minor: Added informative messages for recent-messages API's errors. (#3029) +- Minor: Added section with helpful Chatterino-related links to the About page. (#3068) - Bugfix: Fixed "smiley" emotes being unable to be "Tabbed" with autocompletion, introduced in v2.3.3. (#3010) +- Bugfix: Fixed PubSub not properly trying to resolve pending listens when the pending listens list was larger than 50. (#3037) +- Bugfix: Copy buttons in usercard now show properly in light mode (#3057) +- Bugfix: Fixed comma appended to username completion when not at the beginning of the message. (#3060) +- Bugfix: Fixed bug misplacing chat when zooming on Chrome with Chatterino Native Host extension (#1936) - Dev: Ubuntu packages are now available (#2936) +- Dev: Disabled update checker on Flatpak. (#3051) +- Dev: Add logging for HTTP requests (#2991) ## 2.3.3 diff --git a/conanfile.txt b/conanfile.txt index 57d42da73ce..e4f7eb6bad8 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,6 +1,6 @@ [requires] -openssl/1.1.1d -boost/1.75.0 +openssl/1.1.1k +boost/1.76.0 [generators] qmake diff --git a/resources/buttons/copyDark.png b/resources/buttons/copyDark.png index 2a663bfd6d9..a0b633eec4f 100644 Binary files a/resources/buttons/copyDark.png and b/resources/buttons/copyDark.png differ diff --git a/resources/buttons/copyDark.svg b/resources/buttons/copyDark.svg index ed30f70a720..5fddace4e98 100644 --- a/resources/buttons/copyDark.svg +++ b/resources/buttons/copyDark.svg @@ -9,5 +9,5 @@ height="368.64pt" viewBox="0 0 368.64 368.64"> - + diff --git a/resources/buttons/copyDarkTheme.png b/resources/buttons/copyDarkTheme.png deleted file mode 100644 index e225ee4ea1f..00000000000 --- a/resources/buttons/copyDarkTheme.png +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - Trashcan top - - - - - - - diff --git a/resources/buttons/copyLight.png b/resources/buttons/copyLight.png index a0b633eec4f..2a663bfd6d9 100644 Binary files a/resources/buttons/copyLight.png and b/resources/buttons/copyLight.png differ diff --git a/resources/buttons/copyLight.svg b/resources/buttons/copyLight.svg index 5fddace4e98..ed30f70a720 100644 --- a/resources/buttons/copyLight.svg +++ b/resources/buttons/copyLight.svg @@ -9,5 +9,5 @@ height="368.64pt" viewBox="0 0 368.64 368.64"> - + diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index e84cf632081..c40edc9732b 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -14,7 +14,6 @@ buttons/banRed.png buttons/copyDark.png buttons/copyDark.svg - buttons/copyDarkTheme.png buttons/copyLight.png buttons/copyLight.svg buttons/emote.svg diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp index 7298a9f76ed..d4b48b970de 100644 --- a/src/autogenerated/ResourcesAutogen.cpp +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -15,7 +15,6 @@ Resources2::Resources2() this->buttons.ban = QPixmap(":/buttons/ban.png"); this->buttons.banRed = QPixmap(":/buttons/banRed.png"); this->buttons.copyDark = QPixmap(":/buttons/copyDark.png"); - this->buttons.copyDarkTheme = QPixmap(":/buttons/copyDarkTheme.png"); this->buttons.copyLight = QPixmap(":/buttons/copyLight.png"); this->buttons.menuDark = QPixmap(":/buttons/menuDark.png"); this->buttons.menuLight = QPixmap(":/buttons/menuLight.png"); diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp index f832981ead6..7277cc681e6 100644 --- a/src/autogenerated/ResourcesAutogen.hpp +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -22,7 +22,6 @@ class Resources2 : public Singleton QPixmap ban; QPixmap banRed; QPixmap copyDark; - QPixmap copyDarkTheme; QPixmap copyLight; QPixmap menuDark; QPixmap menuLight; diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 55e5eb9537e..a9945df5e9f 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -10,6 +10,7 @@ #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" +#include "util/Helpers.hpp" #include "util/QStringHash.hpp" #include @@ -156,9 +157,6 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } // Usernames - QString usernamePostfix = - isFirstWord && getSettings()->mentionUsersWithComma ? "," : QString(); - if (prefix.startsWith("@")) { QString usernamePrefix = prefix; @@ -168,8 +166,10 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) for (const auto &name : chatters) { - addString("@" + name + usernamePostfix, - TaggedString::Type::Username); + addString( + "@" + formatUserMention(name, isFirstWord, + getSettings()->mentionUsersWithComma), + TaggedString::Type::Username); } } else if (!getSettings()->userCompletionOnlyWithAt) @@ -178,7 +178,9 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) for (const auto &name : chatters) { - addString(name + usernamePostfix, TaggedString::Type::Username); + addString(formatUserMention(name, isFirstWord, + getSettings()->mentionUsersWithComma), + TaggedString::Type::Username); } } diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 5f90a95a0bc..cb37cf43d40 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -24,6 +24,13 @@ enum class NetworkRequestType { Delete, Patch, }; +const static std::vector networkRequestTypes{ + "GET", // + "POST", // + "PUT", // + "DELETE", // + "PATCH", // +}; // parseHeaderList takes a list of headers in string form, // where each header pair is separated by semicolons (;) and the header name and value is divided by a colon (:) diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index b141cd853e1..c49fcff9755 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -145,6 +145,11 @@ void loadUncached(const std::shared_ptr &data) data->timer_, &QTimer::timeout, worker, [reply, data]() { qCDebug(chatterinoCommon) << "Aborted!"; reply->abort(); + qCDebug(chatterinoHTTP) + << QString("%1 [timed out] %2") + .arg(networkRequestTypes.at( + int(data->requestType_)), + data->request_.url().toString()); if (data->onError_) { @@ -181,6 +186,11 @@ void loadUncached(const std::shared_ptr &data) QNetworkReply::NetworkError::OperationCanceledError) { // Operation cancelled, most likely timed out + qCDebug(chatterinoHTTP) + << QString("%1 [cancelled] %2") + .arg(networkRequestTypes.at( + int(data->requestType_)), + data->request_.url().toString()); return; } @@ -188,6 +198,25 @@ void loadUncached(const std::shared_ptr &data) { auto status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute); + if (data->requestType_ == NetworkRequestType::Get) + { + qCDebug(chatterinoHTTP) + << QString("%1 %2 %3") + .arg(networkRequestTypes.at( + int(data->requestType_)), + QString::number(status.toInt()), + data->request_.url().toString()); + } + else + { + qCDebug(chatterinoHTTP) + << QString("%1 %2 %3 %4") + .arg(networkRequestTypes.at( + int(data->requestType_)), + QString::number(status.toInt()), + data->request_.url().toString(), + QString(data->payload_)); + } // TODO: Should this always be run on the GUI thread? postToThread([data, code = status.toInt()] { data->onError_(NetworkResult({}, code)); @@ -227,6 +256,23 @@ void loadUncached(const std::shared_ptr &data) reply->deleteLater(); + if (data->requestType_ == NetworkRequestType::Get) + { + qCDebug(chatterinoHTTP) + << QString("%1 %2 %3") + .arg(networkRequestTypes.at(int(data->requestType_)), + QString::number(status.toInt()), + data->request_.url().toString()); + } + else + { + qCDebug(chatterinoHTTP) + << QString("%1 %3 %2 %4") + .arg(networkRequestTypes.at(int(data->requestType_)), + data->request_.url().toString(), + QString::number(status.toInt()), + QString(data->payload_)); + } if (data->finally_) { if (data->executeConcurrently_) @@ -286,6 +332,10 @@ void loadCached(const std::shared_ptr &data) 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()) diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 9413afd8f49..a050979694c 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -15,6 +15,7 @@ Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold); Q_LOGGING_CATEGORY(chatterinoEmoji, "chatterino.emoji", logThreshold); Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold); Q_LOGGING_CATEGORY(chatterinoHelper, "chatterino.helper", logThreshold); +Q_LOGGING_CATEGORY(chatterinoHTTP, "chatterino.http", logThreshold); Q_LOGGING_CATEGORY(chatterinoImage, "chatterino.image", logThreshold); Q_LOGGING_CATEGORY(chatterinoIrc, "chatterino.irc", logThreshold); Q_LOGGING_CATEGORY(chatterinoIvr, "chatterino.ivr", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index c3d5c9a98aa..1a74bd9a610 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -12,6 +12,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon); Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji); Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes); Q_DECLARE_LOGGING_CATEGORY(chatterinoHelper); +Q_DECLARE_LOGGING_CATEGORY(chatterinoHTTP); Q_DECLARE_LOGGING_CATEGORY(chatterinoImage); Q_DECLARE_LOGGING_CATEGORY(chatterinoIrc); Q_DECLARE_LOGGING_CATEGORY(chatterinoIvr); diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 012c7ffadd9..af131bbced9 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -3,6 +3,7 @@ #include "providers/twitch/PubsubActions.hpp" #include "providers/twitch/PubsubHelpers.hpp" #include "singletons/Settings.hpp" +#include "util/DebugCount.hpp" #include "util/Helpers.hpp" #include "util/RapidjsonHelpers.hpp" @@ -23,7 +24,8 @@ namespace chatterino { static const char *pingPayload = "{\"type\":\"PING\"}"; -static std::map sentMessages; +static std::map sentListens; +static std::map sentUnlistens; namespace detail { @@ -59,8 +61,9 @@ namespace detail { // This PubSubClient is already at its peak listens return false; } - this->numListens_ += numRequestedListens; + DebugCount::increase("PubSub topic pending listens", + numRequestedListens); for (const auto &topic : message["data"]["topics"].GetArray()) { @@ -68,14 +71,13 @@ namespace detail { Listener{topic.GetString(), false, false, false}); } - auto uuid = generateUuid(); - - rj::set(message, "nonce", uuid); + auto nonce = generateUuid(); + rj::set(message, "nonce", nonce); - std::string payload = rj::stringify(message); - sentMessages[uuid] = payload; + QString payload = rj::stringify(message); + sentListens[nonce] = RequestMessage{payload, numRequestedListens}; - this->send(payload.c_str()); + this->send(payload.toUtf8()); return true; } @@ -103,16 +105,21 @@ namespace detail { return; } - auto message = createUnlistenMessage(topics); + int numRequestedUnlistens = topics.size(); - auto uuid = generateUuid(); + this->numListens_ -= numRequestedUnlistens; + DebugCount::increase("PubSub topic pending unlistens", + numRequestedUnlistens); - rj::set(message, "nonce", generateUuid()); + auto message = createUnlistenMessage(topics); - std::string payload = rj::stringify(message); - sentMessages[uuid] = payload; + auto nonce = generateUuid(); + rj::set(message, "nonce", nonce); - this->send(payload.c_str()); + QString payload = rj::stringify(message); + sentUnlistens[nonce] = RequestMessage{payload, numRequestedUnlistens}; + + this->send(payload.toUtf8()); } void PubSubClient::handlePong() @@ -829,20 +836,17 @@ PubSub::PubSub() // this->signals_.moderation.automodUserMessage.invoke(action); // }; - this->moderationActionHandlers["denied_automod_message"] = [](const auto - &data, - const auto - &roomID) { - // This message got denied by a moderator - // qCDebug(chatterinoPubsub) << QString::fromStdString(rj::stringify(data)); - }; + this->moderationActionHandlers["denied_automod_message"] = + [](const auto &data, const auto &roomID) { + // This message got denied by a moderator + // qCDebug(chatterinoPubsub) << rj::stringify(data); + }; - this->moderationActionHandlers - ["approved_automod_message"] = [](const auto &data, - const auto &roomID) { - // This message got approved by a moderator - // qCDebug(chatterinoPubsub) << QString::fromStdString(rj::stringify(data)); - }; + this->moderationActionHandlers["approved_automod_message"] = + [](const auto &data, const auto &roomID) { + // This message got approved by a moderator + // qCDebug(chatterinoPubsub) << rj::stringify(data); + }; this->websocketClient.set_access_channels(websocketpp::log::alevel::all); this->websocketClient.clear_access_channels( @@ -868,6 +872,13 @@ PubSub::PubSub() void PubSub::addClient() { + if (this->addingClient) + { + return; + } + + this->addingClient = true; + websocketpp::lib::error_code ec; auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, ec); @@ -930,7 +941,7 @@ void PubSub::listenToChannelModerationActions( if (userID.isEmpty()) return; - auto topic = topicFormat.arg(userID).arg(channelID); + auto topic = topicFormat.arg(userID, channelID); if (this->isListeningToTopic(topic)) { @@ -952,7 +963,7 @@ void PubSub::listenToAutomod(const QString &channelID, if (userID.isEmpty()) return; - auto topic = topicFormat.arg(userID).arg(channelID); + auto topic = topicFormat.arg(userID, channelID); if (this->isListeningToTopic(topic)) { @@ -970,7 +981,6 @@ void PubSub::listenToChannelPointRewards(const QString &channelID, static const QString topicFormat("community-points-channel-v1.%1"); assert(!channelID.isEmpty()); assert(account != nullptr); - QString userID = account->getUserId(); auto topic = topicFormat.arg(channelID); @@ -1002,6 +1012,8 @@ void PubSub::listen(rapidjson::Document &&msg) this->requests.emplace_back( std::make_unique(std::move(msg))); + + DebugCount::increase("PubSub topic backlog"); } bool PubSub::tryListen(rapidjson::Document &msg) @@ -1035,26 +1047,27 @@ bool PubSub::isListeningToTopic(const QString &topic) void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr websocketMessage) { - const std::string &payload = websocketMessage->get_payload(); + const auto &payload = + QString::fromStdString(websocketMessage->get_payload()); rapidjson::Document msg; - rapidjson::ParseResult res = msg.Parse(payload.c_str()); + rapidjson::ParseResult res = msg.Parse(payload.toUtf8()); if (!res) { qCDebug(chatterinoPubsub) - << "Error parsing message '" << payload.c_str() - << "' from PubSub:" << rapidjson::GetParseError_En(res.Code()); + << QString("Error parsing message '%1' from PubSub: %2") + .arg(payload, rapidjson::GetParseError_En(res.Code())); return; } if (!msg.IsObject()) { qCDebug(chatterinoPubsub) - << "Error parsing message '" << payload.c_str() - << "' from PubSub. Root object is not an " - "object"; + << QString("Error parsing message '%1' from PubSub. Root object is " + "not an object") + .arg(payload); return; } @@ -1069,7 +1082,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, if (type == "RESPONSE") { - this->handleListenResponse(msg); + this->handleResponse(msg); } else if (type == "MESSAGE") { @@ -1110,6 +1123,9 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, void PubSub::onConnectionOpen(WebsocketHandle hdl) { + DebugCount::increase("PubSub connections"); + this->addingClient = false; + auto client = std::make_shared(this->websocketClient, hdl); @@ -1126,6 +1142,7 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl) const auto &request = *it; if (client->listen(*request)) { + DebugCount::decrease("PubSub topic backlog"); it = this->requests.erase(it); } else @@ -1133,10 +1150,16 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl) ++it; } } + + if (!this->requests.empty()) + { + this->addClient(); + } } void PubSub::onConnectionClose(WebsocketHandle hdl) { + DebugCount::decrease("PubSub connections"); auto clientIt = this->clients.find(hdl); // If this assert goes off, there's something wrong with the connection @@ -1172,33 +1195,70 @@ PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl) return ctx; } -void PubSub::handleListenResponse(const rapidjson::Document &msg) +void PubSub::handleResponse(const rapidjson::Document &msg) { QString error; - if (rj::getSafe(msg, "error", error)) - { - QString nonce; - rj::getSafe(msg, "nonce", nonce); + if (!rj::getSafe(msg, "error", error)) + return; - if (error.isEmpty()) - { - qCDebug(chatterinoPubsub) - << "Successfully listened to nonce" << nonce; - // Nothing went wrong - return; - } + QString nonce; + rj::getSafe(msg, "nonce", nonce); + + const bool failed = !error.isEmpty(); + if (failed) + { qCDebug(chatterinoPubsub) - << "PubSub error:" << error << "on nonce" << nonce; + << QString("Error %1 on nonce %2").arg(error, nonce); + } + + if (auto it = sentListens.find(nonce); it != sentListens.end()) + { + this->handleListenResponse(it->second, failed); return; } + + if (auto it = sentUnlistens.find(nonce); it != sentUnlistens.end()) + { + this->handleUnlistenResponse(it->second, failed); + return; + } + + qCDebug(chatterinoPubsub) + << "Response on unused" << nonce << "client/topic listener mismatch?"; +} + +void PubSub::handleListenResponse(const RequestMessage &msg, bool failed) +{ + DebugCount::decrease("PubSub topic pending listens", msg.topicCount); + if (failed) + { + DebugCount::increase("PubSub topic failed listens", msg.topicCount); + } + else + { + DebugCount::increase("PubSub topic listening", msg.topicCount); + } +} + +void PubSub::handleUnlistenResponse(const RequestMessage &msg, bool failed) +{ + DebugCount::decrease("PubSub topic pending unlistens", msg.topicCount); + if (failed) + { + DebugCount::increase("PubSub topic failed unlistens", msg.topicCount); + } + else + { + DebugCount::decrease("PubSub topic listening", msg.topicCount); + } } void PubSub::handleMessageResponse(const rapidjson::Value &outerData) { QString topic; - qCDebug(chatterinoPubsub) << rj::stringify(outerData).c_str(); + qCDebug(chatterinoPubsub) << rj::stringify(outerData); if (!rj::getSafe(outerData, "topic", topic)) { @@ -1207,7 +1267,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) return; } - std::string payload; + QString payload; if (!rj::getSafe(outerData, "message", payload)) { @@ -1217,19 +1277,19 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) rapidjson::Document msg; - rapidjson::ParseResult res = msg.Parse(payload.c_str()); + rapidjson::ParseResult res = msg.Parse(payload.toUtf8()); if (!res) { qCDebug(chatterinoPubsub) - << "Error parsing message '" << payload.c_str() - << "' from PubSub:" << rapidjson::GetParseError_En(res.Code()); + << QString("Error parsing message '%1' from PubSub: %2") + .arg(payload, rapidjson::GetParseError_En(res.Code())); return; } if (topic.startsWith("whispers.")) { - std::string whisperType; + QString whisperType; if (!rj::getSafe(msg, "type", whisperType)) { @@ -1251,8 +1311,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } else { - qCDebug(chatterinoPubsub) - << "Invalid whisper type:" << whisperType.c_str(); + qCDebug(chatterinoPubsub) << "Invalid whisper type:" << whisperType; return; } } @@ -1262,7 +1321,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) assert(topicParts.length() == 3); const auto &data = msg["data"]; - std::string moderationEventType; + QString moderationEventType; if (!rj::getSafe(msg, "type", moderationEventType)) { @@ -1271,13 +1330,13 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } if (moderationEventType == "moderation_action") { - std::string moderationAction; + QString moderationAction; if (!rj::getSafe(data, "moderation_action", moderationAction)) { qCDebug(chatterinoPubsub) << "Missing moderation action in data:" - << rj::stringify(data).c_str(); + << rj::stringify(data); return; } @@ -1288,7 +1347,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) { qCDebug(chatterinoPubsub) << "No handler found for moderation action" - << moderationAction.c_str(); + << moderationAction; return; } // Invoke handler function @@ -1296,13 +1355,13 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } else if (moderationEventType == "channel_terms_action") { - std::string channelTermsAction; + QString channelTermsAction; if (!rj::getSafe(data, "type", channelTermsAction)) { qCDebug(chatterinoPubsub) << "Missing channel terms action in data:" - << rj::stringify(data).c_str(); + << rj::stringify(data); return; } @@ -1313,7 +1372,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) { qCDebug(chatterinoPubsub) << "No handler found for channel terms action" - << channelTermsAction.c_str(); + << channelTermsAction; return; } // Invoke handler function @@ -1322,7 +1381,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } else if (topic.startsWith("community-points-channel-v1.")) { - std::string pointEventType; + QString pointEventType; if (!rj::getSafe(msg, "type", pointEventType)) { qCDebug(chatterinoPubsub) << "Bad channel point event data"; @@ -1348,7 +1407,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) else { qCDebug(chatterinoPubsub) - << "Invalid point event type:" << pointEventType.c_str(); + << "Invalid point event type:" << pointEventType; } } else if (topic.startsWith("automod-queue.")) @@ -1357,7 +1416,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) assert(topicParts.length() == 3); auto &data = msg["data"]; - std::string automodEventType; + QString automodEventType; if (!rj::getSafe(msg, "type", automodEventType)) { qCDebug(chatterinoPubsub) << "Bad automod event data"; diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index ab068b74380..06c8d50b834 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -47,6 +47,11 @@ using WebsocketErrorCode = websocketpp::lib::error_code; #define MAX_PUBSUB_LISTENS 50 #define MAX_PUBSUB_CONNECTIONS 10 +struct RequestMessage { + QString payload; + int topicCount; +}; + namespace detail { struct Listener { @@ -172,6 +177,7 @@ class PubSub bool isListeningToTopic(const QString &topic); void addClient(); + std::atomic addingClient{false}; State state = State::Connected; @@ -179,12 +185,12 @@ class PubSub std::owner_less> clients; - std::unordered_map> + std::unordered_map< + QString, std::function> moderationActionHandlers; - std::unordered_map> + std::unordered_map< + QString, std::function> channelTermsActionHandlers; void onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr msg); @@ -192,7 +198,9 @@ class PubSub void onConnectionClose(websocketpp::connection_hdl hdl); WebsocketContextPtr onTLSInit(websocketpp::connection_hdl hdl); - void handleListenResponse(const rapidjson::Document &msg); + void handleResponse(const rapidjson::Document &msg); + void handleListenResponse(const RequestMessage &msg, bool failed); + void handleUnlistenResponse(const RequestMessage &msg, bool failed); void handleMessageResponse(const rapidjson::Value &data); void runThread(); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index af8627a6539..416d5ba619b 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -21,7 +21,7 @@ #include "util/QStringHash.hpp" #include "util/RapidjsonHelpers.hpp" -namespace { +namespace chatterino { std::vector getEmoteSetBatches(QStringList emoteSetKeys) { @@ -48,9 +48,6 @@ std::vector getEmoteSetBatches(QStringList emoteSetKeys) return batches; } -} // namespace - -namespace chatterino { TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, const QString &oauthClient, const QString &userID) : Account(ProviderId::Twitch) diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index 4c203dad6f2..af56afaf1cb 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -51,6 +51,8 @@ struct TwitchEmoteSetResolverResponse { } }; +std::vector getEmoteSetBatches(QStringList emoteSetKeys); + class TwitchAccount : public Account { public: diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index 65481ab5d0e..4394da9e6a3 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" @@ -24,8 +25,11 @@ void TwitchBadges::loadTwitchBadges() { assert(this->loaded_ == false); - static QString url( - "https://badges.twitch.tv/v1/badges/global/display?language=en"); + QUrl url("https://badges.twitch.tv/v1/badges/global/display"); + + QUrlQuery urlQuery; + urlQuery.addQueryItem("language", "en"); + url.setQuery(urlQuery); NetworkRequest(url) .onSuccess([this](auto result) -> Outcome { diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 212e6c4e8e7..b8eb0deb1eb 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -786,19 +786,25 @@ void TwitchChannel::loadRecentMessages() return; } - auto baseURL = Env::get().recentMessagesApiUrl.arg(this->getName()); + QUrl url(Env::get().recentMessagesApiUrl.arg(this->getName())); + QUrlQuery urlQuery(url); + if (!urlQuery.hasQueryItem("limit")) + { + urlQuery.addQueryItem( + "limit", QString::number(getSettings()->twitchMessageHistoryLimit)); + } + url.setQuery(urlQuery); - auto url = QString("%1?limit=%2") - .arg(baseURL) - .arg(getSettings()->twitchMessageHistoryLimit); + auto weak = weakOf(this); NetworkRequest(url) - .onSuccess([weak = weakOf(this)](auto result) -> Outcome { + .onSuccess([this, weak](NetworkResult result) -> Outcome { auto shared = weak.lock(); if (!shared) return Failure; - auto messages = parseRecentMessages(result.parseJson(), shared); + auto root = result.parseJson(); + auto messages = parseRecentMessages(root, shared); auto &handler = IrcMessageHandler::instance(); @@ -832,13 +838,38 @@ void TwitchChannel::loadRecentMessages() } } - postToThread( - [shared, messages = std::move(allBuiltMessages)]() mutable { - shared->addMessagesAtStart(messages); - }); + postToThread([this, shared, root, + messages = std::move(allBuiltMessages)]() mutable { + shared->addMessagesAtStart(messages); + + // Notify user about a possible gap in logs if it returned some messages + // but isn't currently joined to a channel + if (QString errorCode = root.value("error_code").toString(); + !errorCode.isEmpty()) + { + qCDebug(chatterinoTwitch) + << QString("rm error_code=%1, channel=%2") + .arg(errorCode, this->getName()); + if (errorCode == "channel_not_joined" && !messages.empty()) + { + shared->addMessage(makeSystemMessage( + "Message history service recovering, there may be " + "gaps in the message history.")); + } + } + }); return Success; }) + .onError([weak](NetworkResult result) { + auto shared = weak.lock(); + if (!shared) + return; + + shared->addMessage(makeSystemMessage( + QString("Message history service unavailable (Error %1)") + .arg(result.status()))); + }) .execute(); } diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index e0fdff803fc..d647fd0d9a1 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -191,7 +191,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { auto app = getApp(); - QString action = root.value("action").toString(); if (action.isNull()) @@ -211,13 +210,20 @@ void NativeMessagingServer::ReceiverThread::handleMessage( AttachedWindow::GetArgs args; args.winId = root.value("winId").toString(); args.yOffset = root.value("yOffset").toInt(-1); - args.x = root.value("size").toObject().value("x").toInt(-1); - args.width = root.value("size").toObject().value("width").toInt(-1); - args.height = root.value("size").toObject().value("height").toInt(-1); + + { + const auto sizeObject = root.value("size").toObject(); + args.x = sizeObject.value("x").toDouble(-1.0); + args.pixelRatio = sizeObject.value("pixelRatio").toDouble(-1.0); + args.width = sizeObject.value("width").toInt(-1); + args.height = sizeObject.value("height").toInt(-1); + } + args.fullscreen = attachFullscreen; qCDebug(chatterinoNativeMessage) - << args.x << args.width << args.height << args.winId; + << args.x << args.pixelRatio << args.width << args.height + << args.winId; if (_type.isNull() || args.winId.isNull()) { diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 13e01bf87ca..2e73d170313 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -1,12 +1,15 @@ -#define LOOKUP_COLOR_COUNT 360 #include "singletons/Theme.hpp" + #include "Application.hpp" +#include "singletons/Resources.hpp" #include #include +#define LOOKUP_COLOR_COUNT 360 + namespace chatterino { Theme::Theme() @@ -80,6 +83,16 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.background = getColor(0, sat, 1); this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); + + // Copy button + if (this->isLightTheme()) + { + this->buttons.copy = getResources().buttons.copyDark; + } + else + { + this->buttons.copy = getResources().buttons.copyLight; + } } void Theme::normalizeColor(QColor &color) diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 99905fa8329..8b55dbab2ca 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -48,6 +48,10 @@ class Theme final : public Singleton, public BaseTheme } input; } splits; + struct { + QPixmap copy; + } buttons; + void normalizeColor(QColor &color); private: diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 01b6e85fd6e..65ee5a24870 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -240,6 +240,12 @@ void Updates::checkForUpdates() return; } + // Disable updates on Flatpak + if (QFileInfo::exists("/.flatpak-info")) + { + return; + } + // Disable updates if on nightly if (Modes::instance().isNightly) { diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 540a136420e..ef53e6814be 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -27,6 +27,20 @@ class DebugCount reinterpret_cast(it.value())++; } } + static void increase(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 decrease(const QString &name) { @@ -42,6 +56,20 @@ class DebugCount reinterpret_cast(it.value())--; } } + static void decrease(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 QString getDebugText() { diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index 5528ac23a5f..2f4d98db998 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -68,4 +68,17 @@ QColor getRandomColor(const QString &userId) return TWITCH_USERNAME_COLORS[colorIndex]; } +QString formatUserMention(const QString &userName, bool isFirstWord, + bool mentionUsersWithComma) +{ + QString result = userName; + + if (isFirstWord && mentionUsersWithComma) + { + result += ","; + } + + return result; +} + } // namespace chatterino diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index b30a3f33135..13247f010bc 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -20,4 +20,14 @@ QString kFormatNumbers(const int &number); QColor getRandomColor(const QString &userId); +/** + * @brief Takes a user's name and some formatting parameter and spits out the standardized way to format it + * + * @param userName a user's name + * @param isFirstWord signifies whether this mention would be the first word in a message + * @param mentionUsersWithComma postfix mentions with a comma. generally powered by getSettings()->mentionUsersWithComma + **/ +QString formatUserMention(const QString &userName, bool isFirstWord, + bool mentionUsersWithComma); + } // namespace chatterino diff --git a/src/util/RapidjsonHelpers.cpp b/src/util/RapidjsonHelpers.cpp index b75f9ca4f9a..fb61219c9c3 100644 --- a/src/util/RapidjsonHelpers.cpp +++ b/src/util/RapidjsonHelpers.cpp @@ -19,13 +19,13 @@ namespace rj { obj.AddMember(rapidjson::Value(key, a).Move(), value.Move(), a); } - std::string stringify(const rapidjson::Value &value) + QString stringify(const rapidjson::Value &value) { rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); value.Accept(writer); - return std::string(buffer.GetString()); + return buffer.GetString(); } bool getSafeObject(rapidjson::Value &obj, const char *key, diff --git a/src/util/RapidjsonHelpers.hpp b/src/util/RapidjsonHelpers.hpp index 6088b44caf7..403bbb5827e 100644 --- a/src/util/RapidjsonHelpers.hpp +++ b/src/util/RapidjsonHelpers.hpp @@ -95,7 +95,7 @@ namespace rj { bool getSafeObject(rapidjson::Value &obj, const char *key, rapidjson::Value &out); - std::string stringify(const rapidjson::Value &value); + QString stringify(const rapidjson::Value &value); } // namespace rj } // namespace chatterino diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 15c1f6dc95e..05392b2d2a0 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -93,6 +93,7 @@ AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) window->fullscreen_ = args.fullscreen; window->x_ = args.x; + window->pixelRatio_ = args.pixelRatio; if (args.height != -1) { @@ -276,7 +277,16 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) // offset int o = this->fullscreen_ ? 0 : 8; - if (this->x_ != -1) + if (this->pixelRatio_ != -1.0) + { + ::MoveWindow( + hwnd, + int(rect.left + this->x_ * scale * this->pixelRatio_ + o - 2), + int(rect.bottom - this->height_ * scale - o), + int(this->width_ * scale), int(this->height_ * scale), true); + } + //support for old extension version 1.3 + else if (this->x_ != -1.0) { ::MoveWindow(hwnd, int(rect.left + this->x_ * scale + o), int(rect.bottom - this->height_ * scale - o), diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index 4ecefa11c0c..186d310fda6 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -17,7 +17,8 @@ class AttachedWindow : public QWidget struct GetArgs { QString winId; int yOffset = -1; - int x = -1; + double x = -1; + double pixelRatio = -1; int width = -1; int height = -1; bool fullscreen = false; @@ -54,7 +55,8 @@ class AttachedWindow : public QWidget void *target_; int yOffset_; int currentYOffset_; - int x_ = -1; + double x_ = -1; + double pixelRatio_ = -1; int width_ = 360; int height_ = -1; bool fullscreen_ = false; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 3c82d43f4f5..1f5c62afec1 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -13,6 +13,7 @@ #include "providers/twitch/api/Kraken.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "util/Clipboard.hpp" #include "util/Helpers.hpp" #include "util/LayoutCreator.hpp" @@ -42,7 +43,7 @@ namespace { { auto label = box.emplace