2023-11-19 12:05:30 +01:00
|
|
|
#include "singletons/ImageUploader.hpp"
|
2019-09-24 16:08:12 +02:00
|
|
|
|
|
|
|
#include "common/Env.hpp"
|
2024-01-15 21:28:44 +01:00
|
|
|
#include "common/network/NetworkRequest.hpp"
|
|
|
|
#include "common/network/NetworkResult.hpp"
|
Sort and force grouping of includes (#4172)
This change enforces strict include grouping using IncludeCategories
In addition to adding this to the .clang-format file and applying it in the tests/src and src directories, I also did the following small changes:
In ChatterSet.hpp, I changed lrucache to a <>include
In Irc2.hpp, I change common/SignalVector.hpp to a "project-include"
In AttachedWindow.cpp, NativeMessaging.cpp, WindowsHelper.hpp, BaseWindow.cpp, and StreamerMode.cpp, I disabled clang-format for the windows-includes
In WindowDescriptors.hpp, I added the missing vector include. It was previously not needed because the include was handled by another file that was previously included first.
clang-format minimum version has been bumped, so Ubuntu version used in the check-formatting job has been bumped to 22.04 (which is the latest LTS)
2022-11-27 19:32:53 +01:00
|
|
|
#include "common/QLogging.hpp"
|
2023-11-19 12:05:30 +01:00
|
|
|
#include "messages/MessageBuilder.hpp"
|
2019-09-24 16:08:12 +02:00
|
|
|
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
2020-06-13 14:53:09 +02:00
|
|
|
#include "singletons/Paths.hpp"
|
|
|
|
#include "singletons/Settings.hpp"
|
2020-07-05 14:32:10 +02:00
|
|
|
#include "util/CombinePath.hpp"
|
2022-12-31 15:41:01 +01:00
|
|
|
#include "widgets/helper/ResizingTextEdit.hpp"
|
2019-09-24 16:08:12 +02:00
|
|
|
|
2020-02-08 16:26:32 +01:00
|
|
|
#include <QBuffer>
|
2019-09-24 18:28:28 +02:00
|
|
|
#include <QHttpMultiPart>
|
2020-07-05 14:32:10 +02:00
|
|
|
#include <QJsonArray>
|
|
|
|
#include <QJsonDocument>
|
2020-02-08 16:26:32 +01:00
|
|
|
#include <QMimeDatabase>
|
|
|
|
#include <QMutex>
|
2023-11-19 12:05:30 +01:00
|
|
|
#include <QPointer>
|
2020-07-05 14:32:10 +02:00
|
|
|
#include <QSaveFile>
|
2019-09-24 16:08:12 +02:00
|
|
|
|
2019-10-11 15:41:33 +02:00
|
|
|
#define UPLOAD_DELAY 2000
|
|
|
|
// Delay between uploads in milliseconds
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
2023-10-08 18:50:48 +02:00
|
|
|
std::optional<QByteArray> convertToPng(const QImage &image)
|
2019-10-11 15:41:33 +02:00
|
|
|
{
|
|
|
|
QByteArray imageData;
|
|
|
|
QBuffer buf(&imageData);
|
|
|
|
buf.open(QIODevice::WriteOnly);
|
|
|
|
bool success = image.save(&buf, "png");
|
|
|
|
if (success)
|
|
|
|
{
|
2023-10-08 18:50:48 +02:00
|
|
|
return imageData;
|
2019-10-11 15:41:33 +02:00
|
|
|
}
|
2023-10-08 18:50:48 +02:00
|
|
|
|
|
|
|
return std::nullopt;
|
2019-10-11 15:41:33 +02:00
|
|
|
}
|
2023-10-08 18:50:48 +02:00
|
|
|
|
2019-10-11 15:41:33 +02:00
|
|
|
} // namespace
|
|
|
|
|
2019-09-24 16:08:12 +02:00
|
|
|
namespace chatterino {
|
2023-10-08 18:50:48 +02:00
|
|
|
|
2020-07-05 14:32:10 +02:00
|
|
|
// logging information on successful uploads to a json file
|
2023-11-19 12:05:30 +01:00
|
|
|
void ImageUploader::logToFile(const QString &originalFilePath,
|
|
|
|
const QString &imageLink,
|
|
|
|
const QString &deletionLink, ChannelPtr channel)
|
2020-06-13 14:53:09 +02:00
|
|
|
{
|
2020-07-05 14:32:10 +02:00
|
|
|
const QString logFileName =
|
|
|
|
combinePath((getSettings()->logPath.getValue().isEmpty()
|
|
|
|
? getPaths()->messageLogDirectory
|
|
|
|
: getSettings()->logPath),
|
|
|
|
"ImageUploader.json");
|
|
|
|
|
|
|
|
//reading existing logs
|
|
|
|
QFile logReadFile(logFileName);
|
|
|
|
bool isLogFileOkay =
|
|
|
|
logReadFile.open(QIODevice::ReadWrite | QIODevice::Text);
|
|
|
|
if (!isLogFileOkay)
|
2020-06-13 14:53:09 +02:00
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2020-07-05 14:32:10 +02:00
|
|
|
QString("Failed to open log file with links at ") + logFileName));
|
2020-06-13 14:53:09 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-07-05 14:32:10 +02:00
|
|
|
auto logs = logReadFile.readAll();
|
|
|
|
if (logs.isEmpty())
|
2020-06-13 14:53:09 +02:00
|
|
|
{
|
2020-07-05 14:32:10 +02:00
|
|
|
logs = QJsonDocument(QJsonArray()).toJson();
|
2020-06-13 14:53:09 +02:00
|
|
|
}
|
2020-07-05 14:32:10 +02:00
|
|
|
logReadFile.close();
|
|
|
|
|
|
|
|
//writing new data to logs
|
|
|
|
QJsonObject newLogEntry;
|
|
|
|
newLogEntry["channelName"] = channel->getName();
|
|
|
|
newLogEntry["deletionLink"] =
|
|
|
|
deletionLink.isEmpty() ? QJsonValue(QJsonValue::Null) : deletionLink;
|
|
|
|
newLogEntry["imageLink"] = imageLink;
|
|
|
|
newLogEntry["localPath"] = originalFilePath.isEmpty()
|
|
|
|
? QJsonValue(QJsonValue::Null)
|
|
|
|
: originalFilePath;
|
|
|
|
newLogEntry["timestamp"] = QDateTime::currentSecsSinceEpoch();
|
|
|
|
// channel name
|
|
|
|
// deletion link (can be empty)
|
2020-06-13 14:53:09 +02:00
|
|
|
// image link
|
2020-07-05 14:32:10 +02:00
|
|
|
// local path to an image (can be empty)
|
2020-06-13 14:53:09 +02:00
|
|
|
// timestamp
|
2020-07-05 14:32:10 +02:00
|
|
|
QSaveFile logSaveFile(logFileName);
|
|
|
|
logSaveFile.open(QIODevice::WriteOnly | QIODevice::Text);
|
|
|
|
QJsonArray entries = QJsonDocument::fromJson(logs).array();
|
|
|
|
entries.push_back(newLogEntry);
|
|
|
|
logSaveFile.write(QJsonDocument(entries).toJson());
|
|
|
|
logSaveFile.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
// extracting link to either image or its deletion from response body
|
|
|
|
QString getJSONValue(QJsonValue responseJson, QString jsonPattern)
|
|
|
|
{
|
|
|
|
for (const QString &key : jsonPattern.split("."))
|
|
|
|
{
|
|
|
|
responseJson = responseJson[key];
|
|
|
|
}
|
|
|
|
return responseJson.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
QString getLinkFromResponse(NetworkResult response, QString pattern)
|
|
|
|
{
|
2021-07-04 13:02:12 +02:00
|
|
|
QRegularExpression regExp("{(.+)}",
|
|
|
|
QRegularExpression::InvertedGreedinessOption);
|
|
|
|
auto match = regExp.match(pattern);
|
|
|
|
|
|
|
|
while (match.hasMatch())
|
2020-07-05 14:32:10 +02:00
|
|
|
{
|
2021-07-04 13:02:12 +02:00
|
|
|
pattern.replace(match.captured(0),
|
|
|
|
getJSONValue(response.parseJson(), match.captured(1)));
|
|
|
|
match = regExp.match(pattern);
|
2020-07-05 14:32:10 +02:00
|
|
|
}
|
|
|
|
return pattern;
|
2020-06-13 14:53:09 +02:00
|
|
|
}
|
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
void ImageUploader::save()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void ImageUploader::sendImageUploadRequest(RawImageData imageData,
|
|
|
|
ChannelPtr channel,
|
|
|
|
QPointer<ResizingTextEdit> textEdit)
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2020-07-05 14:32:10 +02:00
|
|
|
QUrl url(getSettings()->imageUploaderUrl.getValue().isEmpty()
|
|
|
|
? getSettings()->imageUploaderUrl.getDefaultValue()
|
|
|
|
: getSettings()->imageUploaderUrl);
|
|
|
|
QString formField(
|
|
|
|
getSettings()->imageUploaderFormField.getValue().isEmpty()
|
|
|
|
? getSettings()->imageUploaderFormField.getDefaultValue()
|
|
|
|
: getSettings()->imageUploaderFormField);
|
2021-04-17 13:49:19 +02:00
|
|
|
auto extraHeaders =
|
|
|
|
parseHeaderList(getSettings()->imageUploaderHeaders.getValue());
|
2020-06-13 14:53:09 +02:00
|
|
|
QString originalFilePath = imageData.filePath;
|
2019-09-24 16:08:12 +02:00
|
|
|
|
2019-09-24 18:28:28 +02:00
|
|
|
QHttpMultiPart *payload = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
|
|
|
QHttpPart part = QHttpPart();
|
|
|
|
part.setBody(imageData.data);
|
|
|
|
part.setHeader(QNetworkRequest::ContentTypeHeader,
|
2019-10-22 17:21:46 +02:00
|
|
|
QString("image/%1").arg(imageData.format));
|
2019-09-24 18:28:28 +02:00
|
|
|
part.setHeader(QNetworkRequest::ContentLengthHeader,
|
|
|
|
QVariant(imageData.data.length()));
|
2020-05-30 12:30:30 +02:00
|
|
|
part.setHeader(QNetworkRequest::ContentDispositionHeader,
|
|
|
|
QString("form-data; name=\"%1\"; filename=\"control_v.%2\"")
|
2020-07-05 14:32:10 +02:00
|
|
|
.arg(formField)
|
2020-05-30 12:30:30 +02:00
|
|
|
.arg(imageData.format));
|
2019-09-24 18:28:28 +02:00
|
|
|
payload->append(part);
|
2020-07-05 14:32:10 +02:00
|
|
|
|
2019-09-24 16:08:12 +02:00
|
|
|
NetworkRequest(url, NetworkRequestType::Post)
|
2020-07-05 14:32:10 +02:00
|
|
|
.headerList(extraHeaders)
|
2019-09-24 18:28:28 +02:00
|
|
|
.multiPart(payload)
|
2023-11-19 12:05:30 +01:00
|
|
|
.onSuccess(
|
|
|
|
[textEdit, channel, originalFilePath, this](NetworkResult result) {
|
|
|
|
this->handleSuccessfulUpload(result, originalFilePath, channel,
|
|
|
|
textEdit);
|
|
|
|
})
|
|
|
|
.onError([channel, this](NetworkResult result) -> bool {
|
|
|
|
this->handleFailedUpload(result, channel);
|
|
|
|
return true;
|
|
|
|
})
|
|
|
|
.execute();
|
|
|
|
}
|
2019-10-11 15:41:33 +02:00
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
void ImageUploader::handleFailedUpload(const NetworkResult &result,
|
|
|
|
ChannelPtr channel)
|
|
|
|
{
|
|
|
|
auto errorMessage =
|
|
|
|
QString("An error happened while uploading your image: %1")
|
|
|
|
.arg(result.formatError());
|
2020-06-13 14:53:09 +02:00
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
// Try to read more information from the result body
|
|
|
|
auto obj = result.parseJson();
|
|
|
|
if (!obj.isEmpty())
|
|
|
|
{
|
|
|
|
auto apiCode = obj.value("code");
|
|
|
|
if (!apiCode.isUndefined())
|
|
|
|
{
|
|
|
|
auto codeString = apiCode.toVariant().toString();
|
|
|
|
codeString.truncate(20);
|
|
|
|
errorMessage += QString(" - code: %1").arg(codeString);
|
|
|
|
}
|
2022-11-01 21:39:26 +01:00
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
auto apiError = obj.value("error").toString();
|
|
|
|
if (!apiError.isEmpty())
|
|
|
|
{
|
|
|
|
apiError.truncate(300);
|
|
|
|
errorMessage += QString(" - error: %1").arg(apiError.trimmed());
|
|
|
|
}
|
|
|
|
}
|
2022-11-01 21:39:26 +01:00
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
channel->addMessage(makeSystemMessage(errorMessage));
|
|
|
|
this->uploadMutex_.unlock();
|
|
|
|
}
|
2022-11-01 21:39:26 +01:00
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
void ImageUploader::handleSuccessfulUpload(const NetworkResult &result,
|
|
|
|
QString originalFilePath,
|
|
|
|
ChannelPtr channel,
|
|
|
|
QPointer<ResizingTextEdit> textEdit)
|
|
|
|
{
|
|
|
|
if (textEdit == nullptr)
|
|
|
|
{
|
|
|
|
// Split was destroyed abort further uploads
|
|
|
|
|
|
|
|
while (!this->uploadQueue_.empty())
|
|
|
|
{
|
|
|
|
this->uploadQueue_.pop();
|
|
|
|
}
|
|
|
|
this->uploadMutex_.unlock();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
QString link =
|
|
|
|
getSettings()->imageUploaderLink.getValue().isEmpty()
|
|
|
|
? result.getData()
|
|
|
|
: getLinkFromResponse(result, getSettings()->imageUploaderLink);
|
|
|
|
QString deletionLink =
|
|
|
|
getSettings()->imageUploaderDeletionLink.getValue().isEmpty()
|
|
|
|
? ""
|
|
|
|
: getLinkFromResponse(result,
|
|
|
|
getSettings()->imageUploaderDeletionLink);
|
|
|
|
qCDebug(chatterinoImageuploader) << link << deletionLink;
|
|
|
|
textEdit->insertPlainText(link + " ");
|
|
|
|
|
|
|
|
// 2 seconds for the timer that's there not to spam the remote server
|
|
|
|
// and 1 second of actual uploading.
|
|
|
|
auto timeToUpload = this->uploadQueue_.size() * (UPLOAD_DELAY / 1000 + 1);
|
|
|
|
MessageBuilder builder(imageUploaderResultMessage, link, deletionLink,
|
|
|
|
this->uploadQueue_.size(), timeToUpload);
|
|
|
|
channel->addMessage(builder.release());
|
|
|
|
if (this->uploadQueue_.empty())
|
|
|
|
{
|
|
|
|
this->uploadMutex_.unlock();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit, this]() {
|
|
|
|
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
|
|
|
|
textEdit);
|
|
|
|
this->uploadQueue_.pop();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this->logToFile(originalFilePath, link, deletionLink, channel);
|
2019-09-24 16:08:12 +02:00
|
|
|
}
|
|
|
|
|
2023-11-19 12:05:30 +01:00
|
|
|
void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|
|
|
QPointer<ResizingTextEdit> outputTextEdit)
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2023-11-19 12:05:30 +01:00
|
|
|
if (!this->uploadMutex_.tryLock())
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2019-09-25 22:21:26 +02:00
|
|
|
QString("Please wait until the upload finishes.")));
|
2019-09-24 16:08:12 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
channel->addMessage(makeSystemMessage(QString("Started upload...")));
|
2020-06-13 14:53:09 +02:00
|
|
|
if (source->hasUrls())
|
2019-09-25 22:21:26 +02:00
|
|
|
{
|
2020-02-08 16:26:32 +01:00
|
|
|
auto mimeDb = QMimeDatabase();
|
2019-10-11 15:41:33 +02:00
|
|
|
// This path gets chosen when files are copied from a file manager, like explorer.exe, caja.
|
2019-10-19 11:41:23 +02:00
|
|
|
// Each entry in source->urls() is a QUrl pointing to a file that was copied.
|
2019-09-26 14:24:41 +02:00
|
|
|
for (const QUrl &path : source->urls())
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2019-10-22 16:04:36 +02:00
|
|
|
QString localPath = path.toLocalFile();
|
2020-02-08 16:26:32 +01:00
|
|
|
QMimeType mime = mimeDb.mimeTypeForUrl(path);
|
|
|
|
if (mime.name().startsWith("image") && !mime.inherits("image/gif"))
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2019-09-25 22:21:26 +02:00
|
|
|
channel->addMessage(makeSystemMessage(
|
2019-10-22 16:04:36 +02:00
|
|
|
QString("Uploading image: %1").arg(localPath)));
|
|
|
|
QImage img = QImage(localPath);
|
2019-09-25 22:21:26 +02:00
|
|
|
if (img.isNull())
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2019-09-25 22:21:26 +02:00
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("Couldn't load image :(")));
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadMutex_.unlock();
|
2019-09-25 22:21:26 +02:00
|
|
|
return;
|
2019-09-24 16:08:12 +02:00
|
|
|
}
|
2019-09-25 22:21:26 +02:00
|
|
|
|
2023-10-08 18:50:48 +02:00
|
|
|
auto imageData = convertToPng(img);
|
2019-10-11 15:41:33 +02:00
|
|
|
if (imageData)
|
|
|
|
{
|
2023-10-08 18:50:48 +02:00
|
|
|
RawImageData data = {*imageData, "png", localPath};
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadQueue_.push(data);
|
2019-10-11 15:41:33 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2020-06-13 14:53:09 +02:00
|
|
|
QString("Cannot upload file: %1. Couldn't convert "
|
2019-10-11 15:41:33 +02:00
|
|
|
"image to png.")
|
2019-10-22 16:04:36 +02:00
|
|
|
.arg(localPath)));
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadMutex_.unlock();
|
2020-02-10 16:55:59 +01:00
|
|
|
return;
|
2019-10-11 15:41:33 +02:00
|
|
|
}
|
2019-09-25 22:21:26 +02:00
|
|
|
}
|
2020-02-08 16:26:32 +01:00
|
|
|
else if (mime.inherits("image/gif"))
|
2019-09-25 22:21:26 +02:00
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2019-10-22 16:04:36 +02:00
|
|
|
QString("Uploading GIF: %1").arg(localPath)));
|
|
|
|
QFile file(localPath);
|
2019-09-25 22:21:26 +02:00
|
|
|
bool isOkay = file.open(QIODevice::ReadOnly);
|
|
|
|
if (!isOkay)
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2019-09-25 22:21:26 +02:00
|
|
|
channel->addMessage(
|
|
|
|
makeSystemMessage(QString("Failed to open file. :(")));
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadMutex_.unlock();
|
2019-09-25 22:21:26 +02:00
|
|
|
return;
|
2019-09-24 16:08:12 +02:00
|
|
|
}
|
2020-06-13 14:53:09 +02:00
|
|
|
RawImageData data = {file.readAll(), "gif", localPath};
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadQueue_.push(data);
|
2019-09-25 22:21:26 +02:00
|
|
|
file.close();
|
|
|
|
// file.readAll() => might be a bit big but it /should/ work
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
2020-06-13 14:53:09 +02:00
|
|
|
QString("Cannot upload file: %1. Not an image.")
|
2019-10-22 16:04:36 +02:00
|
|
|
.arg(localPath)));
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadMutex_.unlock();
|
2020-02-08 16:26:32 +01:00
|
|
|
return;
|
2019-09-24 16:08:12 +02:00
|
|
|
}
|
|
|
|
}
|
2023-11-19 12:05:30 +01:00
|
|
|
if (!this->uploadQueue_.empty())
|
2019-09-24 16:08:12 +02:00
|
|
|
{
|
2023-11-19 12:05:30 +01:00
|
|
|
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
|
|
|
|
outputTextEdit);
|
|
|
|
this->uploadQueue_.pop();
|
2019-09-24 16:08:12 +02:00
|
|
|
}
|
|
|
|
}
|
2020-06-13 14:53:09 +02:00
|
|
|
else if (source->hasFormat("image/png"))
|
|
|
|
{
|
|
|
|
// the path to file is not present every time, thus the filePath is empty
|
2023-11-19 12:05:30 +01:00
|
|
|
this->sendImageUploadRequest({source->data("image/png"), "png", ""},
|
|
|
|
channel, outputTextEdit);
|
2020-06-13 14:53:09 +02:00
|
|
|
}
|
|
|
|
else if (source->hasFormat("image/jpeg"))
|
|
|
|
{
|
2023-11-19 12:05:30 +01:00
|
|
|
this->sendImageUploadRequest({source->data("image/jpeg"), "jpeg", ""},
|
|
|
|
channel, outputTextEdit);
|
2020-06-13 14:53:09 +02:00
|
|
|
}
|
|
|
|
else if (source->hasFormat("image/gif"))
|
|
|
|
{
|
2023-11-19 12:05:30 +01:00
|
|
|
this->sendImageUploadRequest({source->data("image/gif"), "gif", ""},
|
|
|
|
channel, outputTextEdit);
|
2020-06-13 14:53:09 +02:00
|
|
|
}
|
|
|
|
|
2019-09-24 16:08:12 +02:00
|
|
|
else
|
|
|
|
{ // not PNG, try loading it into QImage and save it to a PNG.
|
2023-10-08 18:50:48 +02:00
|
|
|
auto image = qvariant_cast<QImage>(source->imageData());
|
|
|
|
auto imageData = convertToPng(image);
|
2019-10-11 15:41:33 +02:00
|
|
|
if (imageData)
|
2019-09-25 22:21:26 +02:00
|
|
|
{
|
2023-11-19 12:05:30 +01:00
|
|
|
sendImageUploadRequest({*imageData, "png", ""}, channel,
|
|
|
|
outputTextEdit);
|
2019-10-11 15:41:33 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
channel->addMessage(makeSystemMessage(
|
|
|
|
QString("Cannot upload file, failed to convert to png.")));
|
2023-11-19 12:05:30 +01:00
|
|
|
this->uploadMutex_.unlock();
|
2019-09-25 22:21:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-13 14:53:09 +02:00
|
|
|
|
2019-10-11 15:41:33 +02:00
|
|
|
} // namespace chatterino
|