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 @@
-
-
-
-
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