Networkmanager (#134)
* rename ImageLoader* to Network* * static NetworkManager * NetworkManager queue arbitrary requests * modify urlfetch to use NetworkManager * urlfetchjson in terms of NetworkManager * fetchurljson fetchurltimeout fetchurljsontimeout special fetch url functions with various connects and functions to be called * operate on fetched data in the correct thread * operate on fetched resources in correct thread * networkmanager urlfetch functions * expose urlfetch functions of networkmanager through util and util::twitch * add caller to util functions * cleanup * formatting * urlPut function for NetworkManager and util::twitch * cleanup worker (no more leak) * use urlfetch for LazyLoadedImage::loadImage * Rename NetworkManager methods (#1) * Rename NetworkManager methods Remove unused NetworkManager methods Remove unused NetworkManager includes Reorder includes in lazyloadedimage.cpp and urlfetch.hpp * try to simplify code, might break everything * fixed some more stuff?
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
#include "util/networkmanager.hpp"
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
namespace chatterino {
|
||||
namespace util {
|
||||
|
||||
QThread NetworkManager::workerThread;
|
||||
QNetworkAccessManager NetworkManager::NaM;
|
||||
|
||||
void NetworkManager::init()
|
||||
{
|
||||
NetworkManager::NaM.moveToThread(&NetworkManager::workerThread);
|
||||
NetworkManager::workerThread.start();
|
||||
}
|
||||
|
||||
void NetworkManager::deinit()
|
||||
{
|
||||
NetworkManager::workerThread.quit();
|
||||
NetworkManager::workerThread.wait();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,233 @@
|
||||
#pragma once
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace messages {
|
||||
|
||||
class LazyLoadedImage;
|
||||
|
||||
} // namespace messages
|
||||
|
||||
namespace util {
|
||||
|
||||
class NetworkRequest
|
||||
{
|
||||
struct Data {
|
||||
QNetworkRequest request;
|
||||
const QObject *caller = nullptr;
|
||||
std::function<void(QNetworkReply *)> onReplyCreated;
|
||||
} data;
|
||||
|
||||
public:
|
||||
NetworkRequest() = delete;
|
||||
|
||||
explicit NetworkRequest(const char *url)
|
||||
{
|
||||
this->data.request.setUrl(QUrl(url));
|
||||
}
|
||||
|
||||
explicit NetworkRequest(const std::string &url)
|
||||
{
|
||||
this->data.request.setUrl(QUrl(QString::fromStdString(url)));
|
||||
}
|
||||
|
||||
explicit NetworkRequest(const QString &url)
|
||||
{
|
||||
this->data.request.setUrl(QUrl(url));
|
||||
}
|
||||
|
||||
void setCaller(const QObject *_caller)
|
||||
{
|
||||
this->data.caller = _caller;
|
||||
}
|
||||
|
||||
void setOnReplyCreated(std::function<void(QNetworkReply *)> f)
|
||||
{
|
||||
this->data.onReplyCreated = f;
|
||||
}
|
||||
|
||||
void setRawHeader(const QByteArray &headerName, const QByteArray &value)
|
||||
{
|
||||
this->data.request.setRawHeader(headerName, value);
|
||||
}
|
||||
|
||||
template <typename FinishedCallback>
|
||||
void get(FinishedCallback onFinished)
|
||||
{
|
||||
NetworkRequester requester;
|
||||
NetworkWorker *worker = new NetworkWorker;
|
||||
|
||||
worker->moveToThread(&NetworkManager::workerThread);
|
||||
|
||||
if (this->data.caller != nullptr) {
|
||||
QObject::connect(worker, &NetworkWorker::doneUrl, this->data.caller,
|
||||
[onFinished](auto reply) {
|
||||
onFinished(reply);
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
&requester, &NetworkRequester::requestUrl, worker,
|
||||
[ data = std::move(this->data), worker, onFinished{std::move(onFinished)} ]() {
|
||||
QNetworkReply *reply = NetworkManager::NaM.get(data.request);
|
||||
|
||||
if (data.onReplyCreated) {
|
||||
data.onReplyCreated(reply);
|
||||
}
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, worker,
|
||||
[ data = std::move(this->data), worker, reply, onFinished ]() {
|
||||
if (data.caller == nullptr) {
|
||||
onFinished(reply);
|
||||
|
||||
reply->deleteLater();
|
||||
} else {
|
||||
emit worker->doneUrl(reply);
|
||||
}
|
||||
|
||||
delete worker;
|
||||
});
|
||||
});
|
||||
|
||||
emit requester.requestUrl();
|
||||
}
|
||||
};
|
||||
|
||||
class NetworkWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void doneUrl(QNetworkReply *);
|
||||
};
|
||||
|
||||
class NetworkRequester : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void requestUrl();
|
||||
};
|
||||
|
||||
class NetworkManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static QThread workerThread;
|
||||
static QNetworkAccessManager NaM;
|
||||
|
||||
static void init();
|
||||
static void deinit();
|
||||
|
||||
template <typename FinishedCallback>
|
||||
static void urlFetch(QNetworkRequest request, FinishedCallback onFinished)
|
||||
{
|
||||
NetworkRequester requester;
|
||||
NetworkWorker *worker = new NetworkWorker;
|
||||
|
||||
worker->moveToThread(&NetworkManager::workerThread);
|
||||
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
|
||||
[ onFinished = std::move(onFinished), request = std::move(request) ]() {
|
||||
QNetworkReply *reply = NetworkManager::NaM.get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished,
|
||||
[onFinished, reply, worker]() {
|
||||
onFinished(reply);
|
||||
delete worker;
|
||||
});
|
||||
});
|
||||
|
||||
emit requester.requestUrl();
|
||||
}
|
||||
|
||||
template <typename FinishedCallback>
|
||||
static void urlFetch(const QUrl &url, FinishedCallback onFinished)
|
||||
{
|
||||
urlFetch(QNetworkRequest(url), std::move(onFinished));
|
||||
}
|
||||
|
||||
template <typename Callback, typename ReplyCreatedCallback = void (*)(QNetworkReply *)>
|
||||
static void urlFetch(QNetworkRequest request, const QObject *caller, Callback callback,
|
||||
ReplyCreatedCallback onReplyCreated = [](QNetworkReply *) { return; })
|
||||
{
|
||||
NetworkRequester requester;
|
||||
NetworkWorker *worker = new NetworkWorker;
|
||||
|
||||
worker->moveToThread(&NetworkManager::workerThread);
|
||||
|
||||
QObject::connect(&requester, &NetworkRequester::requestUrl, worker, [=]() {
|
||||
QNetworkReply *reply = NetworkManager::NaM.get(request);
|
||||
|
||||
onReplyCreated(reply);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, worker,
|
||||
[=]() { emit worker->doneUrl(reply); });
|
||||
});
|
||||
|
||||
QObject::connect(worker, &NetworkWorker::doneUrl, caller, [=](QNetworkReply *reply) {
|
||||
callback(reply);
|
||||
delete worker;
|
||||
});
|
||||
emit requester.requestUrl();
|
||||
}
|
||||
|
||||
template <typename Callback, typename ReplyCreatedCallback = void (*)(QNetworkReply *)>
|
||||
static void urlFetch(const QUrl &url, const QObject *caller, Callback callback,
|
||||
ReplyCreatedCallback onReplyCreated = [](QNetworkReply *) { return; })
|
||||
{
|
||||
urlFetch(QNetworkRequest(url), caller, callback, onReplyCreated);
|
||||
}
|
||||
|
||||
template <typename FinishedCallback>
|
||||
static void urlPut(QNetworkRequest request, FinishedCallback onFinished, QByteArray *data)
|
||||
{
|
||||
NetworkRequester requester;
|
||||
NetworkWorker *worker = new NetworkWorker;
|
||||
|
||||
worker->moveToThread(&NetworkManager::workerThread);
|
||||
QObject::connect(
|
||||
&requester, &NetworkRequester::requestUrl, worker,
|
||||
[ onFinished = std::move(onFinished), request = std::move(request), data ]() {
|
||||
QNetworkReply *reply = NetworkManager::NaM.put(request, *data);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished,
|
||||
[ onFinished = std::move(onFinished), reply ]() {
|
||||
onFinished(reply);
|
||||
delete worker;
|
||||
});
|
||||
});
|
||||
|
||||
emit requester.requestUrl();
|
||||
}
|
||||
|
||||
template <typename FinishedCallback>
|
||||
static void urlPut(QNetworkRequest request, FinishedCallback onFinished)
|
||||
{
|
||||
NetworkRequester requester;
|
||||
NetworkWorker *worker = new NetworkWorker;
|
||||
|
||||
worker->moveToThread(&NetworkManager::workerThread);
|
||||
QObject::connect(
|
||||
&requester, &NetworkRequester::requestUrl, worker,
|
||||
[ onFinished = std::move(onFinished), request = std::move(request), worker ]() {
|
||||
QNetworkReply *reply = NetworkManager::NaM.put(request, "");
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished,
|
||||
[ onFinished = std::move(onFinished), reply, worker ]() {
|
||||
onFinished(reply);
|
||||
delete worker;
|
||||
});
|
||||
});
|
||||
|
||||
emit requester.requestUrl();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
} // namespace chatterino
|
||||
+121
-193
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "credentials.hpp"
|
||||
#include "accountmanager.hpp"
|
||||
#include "credentials.hpp"
|
||||
#include "util/networkmanager.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
@@ -14,68 +15,128 @@
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace chatterino {
|
||||
namespace util {
|
||||
|
||||
namespace twitch {
|
||||
|
||||
static void get(QString url, std::function<void(QJsonObject &)> successCallback)
|
||||
static QJsonObject parseJSONFromReply(QNetworkReply *reply)
|
||||
{
|
||||
auto manager = new QNetworkAccessManager();
|
||||
if (reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
|
||||
request.setRawHeader("Client-ID", getDefaultClientID());
|
||||
request.setRawHeader("Accept", "application/vnd.twitchtv.v5+json");
|
||||
if (jsonDoc.isNull()) {
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager->get(request);
|
||||
return jsonDoc.object();
|
||||
}
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [=] {
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
|
||||
if (!jsonDoc.isNull()) {
|
||||
QJsonObject rootNode = jsonDoc.object();
|
||||
|
||||
successCallback(rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
static void urlFetchJSON(const QString &url, const QObject *caller,
|
||||
std::function<void(QJsonObject &)> successCallback)
|
||||
{
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(caller);
|
||||
req.get([=](QNetworkReply *reply) {
|
||||
auto node = parseJSONFromReply(reply);
|
||||
successCallback(node);
|
||||
});
|
||||
}
|
||||
|
||||
static void getUserID(QString username, std::function<void(QString)> successCallback)
|
||||
static void urlFetchTimeout(const QString &url, const QObject *caller,
|
||||
std::function<void(QNetworkReply *)> successCallback, int timeoutMs)
|
||||
{
|
||||
get("https://api.twitch.tv/kraken/users?login=" + username, [=](const QJsonObject &root) {
|
||||
if (!root.value("users").isArray()) {
|
||||
qDebug() << "API Error while getting user id, users is not an array";
|
||||
return;
|
||||
QTimer *timer = new QTimer;
|
||||
timer->setSingleShot(true);
|
||||
|
||||
QEventLoop *loop = new QEventLoop;
|
||||
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(loop);
|
||||
req.setOnReplyCreated([loop, timer](QNetworkReply *reply) {
|
||||
QObject::connect(timer, &QTimer::timeout, loop, [=]() {
|
||||
QObject::disconnect(reply, &QNetworkReply::finished, loop, &QEventLoop::quit);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
});
|
||||
});
|
||||
req.get([=](QNetworkReply *reply) {
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
successCallback(reply);
|
||||
}
|
||||
|
||||
auto users = root.value("users").toArray();
|
||||
if (users.size() != 1) {
|
||||
qDebug() << "API Error while getting user id, users array size is not 1";
|
||||
return;
|
||||
}
|
||||
if (!users[0].isObject()) {
|
||||
qDebug() << "API Error while getting user id, first user is not an object";
|
||||
return;
|
||||
}
|
||||
auto firstUser = users[0].toObject();
|
||||
auto id = firstUser.value("_id");
|
||||
if (!id.isString()) {
|
||||
qDebug()
|
||||
<< "API Error: while getting user id, first user object `_id` key is not a string";
|
||||
return;
|
||||
}
|
||||
successCallback(id.toString());
|
||||
reply->deleteLater();
|
||||
loop->quit();
|
||||
});
|
||||
|
||||
QObject::connect(timer, SIGNAL(timeout()), loop, SLOT(quit()));
|
||||
|
||||
timer->start(timeoutMs);
|
||||
loop->exec();
|
||||
delete timer;
|
||||
delete loop;
|
||||
}
|
||||
|
||||
static void urlFetchJSONTimeout(const QString &url, const QObject *caller,
|
||||
std::function<void(QJsonObject &)> successCallback, int timeoutMs)
|
||||
{
|
||||
urlFetchTimeout(url, caller,
|
||||
[=](QNetworkReply *reply) {
|
||||
auto node = parseJSONFromReply(reply);
|
||||
successCallback(node);
|
||||
},
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
namespace twitch {
|
||||
|
||||
static void get(QString url, const QObject *caller,
|
||||
std::function<void(QJsonObject &)> successCallback)
|
||||
{
|
||||
util::NetworkRequest req(url);
|
||||
req.setCaller(caller);
|
||||
req.setRawHeader("Client-ID", getDefaultClientID());
|
||||
req.setRawHeader("Accept", "application/vnd.twitchtv.v5+json");
|
||||
req.get([=](QNetworkReply *reply) {
|
||||
auto node = parseJSONFromReply(reply);
|
||||
successCallback(node);
|
||||
});
|
||||
}
|
||||
|
||||
static void getUserID(QString username, const QObject *caller,
|
||||
std::function<void(QString)> successCallback)
|
||||
{
|
||||
get("https://api.twitch.tv/kraken/users?login=" + username, caller,
|
||||
[=](const QJsonObject &root) {
|
||||
if (!root.value("users").isArray()) {
|
||||
qDebug() << "API Error while getting user id, users is not an array";
|
||||
return;
|
||||
}
|
||||
|
||||
auto users = root.value("users").toArray();
|
||||
if (users.size() != 1) {
|
||||
qDebug() << "API Error while getting user id, users array size is not 1";
|
||||
return;
|
||||
}
|
||||
if (!users[0].isObject()) {
|
||||
qDebug() << "API Error while getting user id, first user is not an object";
|
||||
return;
|
||||
}
|
||||
auto firstUser = users[0].toObject();
|
||||
auto id = firstUser.value("_id");
|
||||
if (!id.isString()) {
|
||||
qDebug() << "API Error: while getting user id, first user object `_id` key is not "
|
||||
"a string";
|
||||
return;
|
||||
}
|
||||
successCallback(id.toString());
|
||||
});
|
||||
}
|
||||
static void put(QUrl url, std::function<void(QJsonObject)> successCallback)
|
||||
{
|
||||
@@ -84,157 +145,24 @@ static void put(QUrl url, std::function<void(QJsonObject)> successCallback)
|
||||
|
||||
request.setRawHeader("Client-ID", getDefaultClientID());
|
||||
request.setRawHeader("Accept", "application/vnd.twitchtv.v5+json");
|
||||
request.setRawHeader("Authorization", "OAuth " +
|
||||
AccountManager::getInstance().getTwitchUser().getOAuthToken().toUtf8());
|
||||
QNetworkReply *reply = manager->put(request,"");
|
||||
QObject::connect(reply, &QNetworkReply::finished, [=](){
|
||||
if(reply->error() == QNetworkReply::NetworkError::NoError)
|
||||
{
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
if(!jsonDoc.isNull())
|
||||
{
|
||||
QJsonObject rootNode = jsonDoc.object();
|
||||
request.setRawHeader(
|
||||
"Authorization",
|
||||
"OAuth " + AccountManager::getInstance().getTwitchUser().getOAuthToken().toUtf8());
|
||||
|
||||
successCallback(rootNode);
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
NetworkManager::urlPut(std::move(request), [=](QNetworkReply *reply) {
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
if (!jsonDoc.isNull()) {
|
||||
QJsonObject rootNode = jsonDoc.object();
|
||||
|
||||
successCallback(rootNode);
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace twitch
|
||||
|
||||
static void urlFetch(const QString &url, std::function<void(QNetworkReply &)> successCallback,
|
||||
QNetworkAccessManager *manager = nullptr)
|
||||
{
|
||||
bool customManager = true;
|
||||
|
||||
if (manager == nullptr) {
|
||||
manager = new QNetworkAccessManager();
|
||||
customManager = false;
|
||||
}
|
||||
|
||||
QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
|
||||
QNetworkReply *reply = manager->get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [=] {
|
||||
/* uncomment to follow redirects
|
||||
QVariant replyStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
if (replyStatus >= 300 && replyStatus <= 304) {
|
||||
QString newUrl =
|
||||
reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
|
||||
urlFetch(newUrl, successCallback);
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
successCallback(*reply);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
if (!customManager) {
|
||||
manager->deleteLater();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void urlFetchJSON(const QString &url, std::function<void(QJsonObject &)> successCallback,
|
||||
QNetworkAccessManager *manager = nullptr)
|
||||
{
|
||||
urlFetch(url,
|
||||
[=](QNetworkReply &reply) {
|
||||
QByteArray data = reply.readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
|
||||
if (jsonDoc.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject rootNode = jsonDoc.object();
|
||||
|
||||
successCallback(rootNode);
|
||||
},
|
||||
manager);
|
||||
}
|
||||
|
||||
static void urlFetchTimeout(const QString &url,
|
||||
std::function<void(QNetworkReply &)> successCallback, int timeoutMs,
|
||||
QNetworkAccessManager *manager = nullptr)
|
||||
{
|
||||
bool customManager = true;
|
||||
|
||||
if (manager == nullptr) {
|
||||
manager = new QNetworkAccessManager();
|
||||
customManager = false;
|
||||
}
|
||||
|
||||
QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
|
||||
QNetworkReply *reply = manager->get(request);
|
||||
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
|
||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
QObject::connect(reply, &QNetworkReply::finished, [=] {
|
||||
/* uncomment to follow redirects
|
||||
QVariant replyStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
if (replyStatus >= 300 && replyStatus <= 304) {
|
||||
QString newUrl =
|
||||
reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
|
||||
urlFetch(newUrl, successCallback);
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||
successCallback(*reply);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
if (!customManager) {
|
||||
manager->deleteLater();
|
||||
}
|
||||
});
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (!timer.isActive()) {
|
||||
qDebug() << "TIMED OUT";
|
||||
QObject::disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
reply->abort();
|
||||
} else {
|
||||
// qDebug() << "XDDD HEHEHE";
|
||||
}
|
||||
}
|
||||
|
||||
static void urlFetchJSONTimeout(const QString &url,
|
||||
std::function<void(QJsonObject &)> successCallback, int timeoutMs,
|
||||
QNetworkAccessManager *manager = nullptr)
|
||||
{
|
||||
urlFetchTimeout(url,
|
||||
[=](QNetworkReply &reply) {
|
||||
QByteArray data = reply.readAll();
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
|
||||
|
||||
if (jsonDoc.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject rootNode = jsonDoc.object();
|
||||
|
||||
successCallback(rootNode);
|
||||
},
|
||||
timeoutMs, manager);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace chatterino
|
||||
|
||||
Reference in New Issue
Block a user